From 3c0e7b2c50996c5657063a8429669102273cbd8a Mon Sep 17 00:00:00 2001 From: Robert Drinovac <52541649+RobertD502@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:40:54 -0500 Subject: [PATCH 01/11] rewrite --- README.md | 245 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 197 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 5b3b612..0654054 100644 --- a/README.md +++ b/README.md @@ -9,36 +9,45 @@ ___ -Custom Home Assistant component for controlling and monitoring PetKit devices. +Custom Home Assistant component for controlling and monitoring PetKit devices and pets. `Currently Supported Devices:` +- [Fresh Element Infinity](https://www.amazon.com/PETKIT-Automatic-Stainless-Programmable-Dispenser/dp/B09JFK8BCQ) - [Fresh Element Solo](https://www.amazon.com/PETKIT-Automatic-Dispenser-Compatible-Freeze-Dried/dp/B09158J9PF/) - [Fresh Element Mini Pro](https://www.amazon.com/PETKIT-Automatic-Stainless-Indicator-Dispenser-2-8L/dp/B08GS1CPHH/) - [Eversweet 3 Pro Water Fountain](https://www.amazon.com/PETKIT-Wireless-Fountain-Stainless-Dispenser/dp/B09QRH6L3M/) +- [Pura X Litter Box](https://www.amazon.com/PETKIT-Self-Cleaning-Scooping-Automatic-Multiple/dp/B08T9CCP1M) ## **Prior To Installation** -**This integration requires using your PetKit account `email` and `password`.** +- **This integration requires using your PetKit account `email` and `password`.** -**If using another PetKit integration that uses the petkit domain, you will need to delete it prior to installing this integration.** +- **If using another PetKit integration that uses the petkit domain, you will need to delete it prior to installing this integration.** -**The current polling interval is set to 2 minutes. If you would like to set a different polling interval, change the `DEFAULT_SCAN_INTERVAL` in the constants.py file (in seconds).** +- **The current polling interval is set to 2 minutes. If you would like to set a different polling interval, change the `DEFAULT_SCAN_INTERVAL` in the constants.py file (in seconds).** -### If you don't have a water fountain on your PetKit account: +## Important - Please Read: +### Note About PetKit Account: -Create a new PetKit account and share your devices from your original account to it. This will allow you to use your main account on the mobile app and the secondary account with this integration. Otherwise, your main account will get logged out of the mobile app when using this integration. This is a limitation of how PetKit handles authorization. +PetKit accounts can only be logged in on one device at a time. Using this integration will result in getting signed out of the mobile app. You can avoid this by creating a secondary account and sharing devices from the main account **(except water fountains)**. However, some device functionality is lost when using a secondary account as well as not being able to share pets between accounts. **Therefore, you will get the most out of this integration by using your original account.** +### If you have a water fountain: -### If you do have a water fountain: +`Note #1:` Getting the most recent data from your water fountain, as well as controlling the water fountain, requires that the BLE relay is set up within the PetKit app. Otherwise, you will be limited to data that isn't up-to-date and no ability to control the water fountain as it requires another compatible PetKit device acting as a BLE relay. -Unfortunately, there is no way of sharing a water fountain with a secondary account. As a result, you will need to use your main PetKit account to pull in water fountain data. When doing so, your main account will get signed out of any mobile app it is currently signed in on. This is a limitation of how PetKit handles authorization. If you only want to pull in data for non-water fountain devices, see "If you don't have a water fountain on your PetKit account" above. +`Note #2:` If you have the BLE relay set up, please be sure to follow these direction prior to using the integration: +- Sign out of the PetKit app and turn off bluetooth on your phone, tablet, etc. +- With bluetooth turned off, force close the PetKit app (be sure to do this on any device that was using your account with the PetKit app). +- Unplug all PetKit devices. Once all are off, you may plug them in again. +- With all devices powered back on, you can turn bluetooth back on (on your mobile device or tablet). +- Proceed with the installation and setup instructions for this integration. +> Although these steps may seem tedious, they are a necessary evil. The PetKit app will ping the bluetooth endpoints and initiate the relay even with your account signed out. The only way of giving the integration control/the ability to initiate the relay is to sever any bluetooth connection that the app has started. -`Note #1:` When using your main PetKit account: as long as you don't reopen the PetKit app, your notification token will remain valid allowing you to receive notifications from the PetKit app even though your account is technically signed out of the app. -`Note #2:` If you want to use the PetKit app momentarily to change some settings, be sure to disable the PetKit integration before logging into the app (If you don't, you will be asked to reauthenticate). Once you are done making changes within the app, re-enable the integration. - -`Note #3:` Getting the most recent data from your water fountain, as well as controlling the water fountain, requires that the BLE relay is set up within the PetKit app. Otherwise, you will be limited to data that isn't up-to-date and no ability to control the water fountain as it requires another compatible PetKit device acting as a BLE relay. +### Using the PetKit app: +If you want to use the PetKit app momentarily to change some settings, be sure to disable the PetKit integration before logging into the app. If you don't, you will be asked to reauthenticate. Once you are done making changes within the app, re-enable the integration. +> If you needed the BLE relay while using the app (bluetooth turned on on your device), please be sure to follow the steps in `Note #2` above in order to sever the BLE relay connection started by the PetKit app. # Installation @@ -68,55 +77,195 @@ Alternatively, follow the steps below: # Devices -Each PetKit device is represented as a device in Home Assistant. Within each device +Each PetKit device and pet is represented as a device in Home Assistant. Within each device are several entities described below. ## Feeders +___ + +
+ Fresh Element Infinity (click to expand) + + +Each Feeder has the following entities: + +| Entity | Entity Type | Additional Comments | +|-----------------------------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Call pet` | `Button` | Only available if your feeder is online (connected to PetKit's servers). | +| `Cancel manual feed` | `Button` | - Only available if your feeder is online (connected to PetKit's servers).
- Will cancel a manual feeding that is currently in progress. | +| `Indicator light` | `Switch` | Only available if your feeder is online (connected to PetKit's servers). | +| `Manual Feed` | `Number` | - Only available if your feeder is online (connected to PetKit's servers).
- Select the amount of food, in grams, you'd like to dispense immediately.
- Note: valid amount ranges from 5-200 grams in increments of 1 gram. | +| `Desiccant days remaining` | `Sensor` | Number of days left before the desiccant needs to be replaced. | +| `Food level` | `Binary Sensor` | Allows for determining if there is a food shortage. | +| `Child lock` | `Switch` | Only available if your feeder is online (connected to PetKit's servers). | +| `Do not disturb` | `Switch` | Only available if your feeder is online (connected to PetKit's servers). | +| `Reset desiccant` | `Button` | - Allows you to reset the desiccant back to 30 days after replacing it.
- Only available if your feeder is online (connected to PetKit's servers). | +| `Sound` | `Select` | - Allows you to select which sound is played when calling your pet.
- Default sound is selected if no self-recorded sounds are available or selected.
- Only available if your feeder is online (connected to PetKit's servers). | +| `Surplus` | `Number` | - Allows for selecting weight in bowl that is considered to be surplus.
- If a surplus amount is selected and surplus control is enabled, planned feedings will stop when the weight of food detected in the bowl is equal to the surplus amount.
- Only available if your feeder is online (connected to PetKit's servers). | +| `Surplus control` | `Switch` | - Enable or disable surplus control.
- Only available if your feeder is online (connected to PetKit's servers). | +| `System notification sound` | `Switch` | - Enable or disable system notification sound.
- Only available if your feeder is online (connected to PetKit's servers). | +| `Voice with dispense` | `Switch` | - Enable or disable sound (selected in Sound entity) to play with every feeding.
- Only available if your feeder is online (connected to PetKit's servers). | +| `Volume` | `Number` | - Allows for controlling feeder sound volume.
- Only available if your feeder is online (connected to PetKit's servers). | +| `Amount eaten` | `Sensor` | Amount of food your pet has eaten from the bowl today. | +| `Battery` | `Binary Sensor` | Indicates if your battery is charging or not charging. | +| `Battery status` | `Sensor` | - Will only become available when feeder is running on batteries.
- Indicates the battery level (Normal or Low). | +| `Dispensed` | `Sensor` | Amount of food, in grams, dispensed today. | +| `Error` | `Sensor` | Identifies any errors reported by the feeder. | +| `Food in bowl` | `Sensor` | Amount of food, in grams, that is currently in the bowl. | +| `Planned` | `Sensor` | Amount of food, in grams, that the feeder plans to dispense today. | +| `Planned dispensed` | `Sensor` | Of the planned amount that is to be dispensed today, amount of food (grams) that has been dispensed. | +| `RSSI` | `Sensor` | WiFi connection strength. | +| `Status` | `Sensor` | `Normal` = Feeder is connected to PetKit's servers
`Offline` = Feeder is not connected to PetKit servers
`On Batteries` = Feeder is currently being powered by the batteries. | +| `Times dispensed` | `Sensor` | Number of times food has been dispensed today. | +| `Times eaten` | `Sensor` | Number of times pet ate today. | +
+ +
+ Fresh Element Solo (click to expand) + Each Feeder has the following entities: -| Entity | Entity Type | Additional Comments | -|----------------------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `Child lock` | `Switch` | Only available if your feeder is online (connected to PetKit's servers). | -| `Dispense tone` | `Switch` | -`Only available for Fresh Element Solo feeders.`
- Only available if your feeder is online (connected to PetKit's servers). | -| `Food shortage alarm` | `Switch` | -`Only available for Fresh Element Solo feeders.`
- Only available if your feeder is online (connected to PetKit's servers). | -| `Indicator light` | `Switch` | This entity is only available if your feeder is online (connected to PetKit's servers). | -| `Manual feed` | `Select` | - Allows setting the amount of food to dispense immediately - amount allowed is personalized to the type of feeder.
- Only available if your feeder is online (connected to PetKit's servers). | -| `Food level` | `Binary Sensor` | Allows for determining if there is a food shortage. | -| `Cancel manual feed` | `Button` | -`Only available for Fresh Element Solo feeders.`
- Allows you to cancel a manual feeding that is currently dispensing. | -| `Reset desiccant` | `Button` | - Allows you to reset the desiccant back to 30 days after replacing it.
- Only available if your feeder is online (connected to PetKit's servers). | -| `Battery installed` | `Binary Sensor` | -`Only available for Fresh Element Solo feeders. - Mini Feeders always report the same value regardless of if batteries are installed or not`.
- If batteries are removed or installed, power cycling the feeder is required for the status to update. | -| `Battery status` | `Sensor` | -`Will only become available when feeder is running on batteries.`
- Indicates the battery level (Normal or Low).
-`Friendly Note:` Mini feeders have a bug when batteries are installed that has never been addressed by PetKit. This can result in your device locking up until the batteries are removed and feeder is power cycled. I am not sure what triggers this bug, but as a safety measure I don't install batteries into mini feeders. | -| `Desiccant days remaining` | `Sensor` | Number of days left before the desiccant needs to be replaced. | -| `Dispensed` | `Sensor` | -`Only available for Fresh Element Solo feeders.`
- Amount of food, in grams, dispensed today. | -| `Manually dispensed` | `Sensor` | -`Only available for Fresh Element Solo feeders.`
- Amount of food, in grams, manually dispensed today. | -| `Planned` | `Sensor` | -`Only available for Fresh Element Solo feeders.`
- Amount of food, in grams, that the feeder plans to dispense today. | -| `Planned dispensed` | `Sensor` | -`Only available for Fresh Element Solo feeders.`
- Of the planned amount that is to be dispensed today, amount of food (grams) that has been dispensed. | -| `RSSI` | `Sensor` | WiFi connection strength. | -| `Status` | `Sensor` | `Normal` = Feeder is connected to PetKit's servers
`Offline` = Feeder is not connected to PetKit servers
`On Batteries` = If installed, feeder is currently being powered by the batteries. | -| `Times dispensed` | `Sensor` | -`Only available for Fresh Element Solo feeders.`
- Number of times food has been dispensed today. | - - -## Eversweet 3 Pro Water Fountain +| Entity | Entity Type | Additional Comments | +|-----------------------------|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Cancel manual feed` | `Button` | - Only available if your feeder is online (connected to PetKit's servers).
- Will cancel a manual feeding that is currently in progress. | +| `Indicator light` | `Switch` | Only available if your feeder is online (connected to PetKit's servers). | +| `Manual feed` | `Select` | - Allows setting the amount of food to dispense immediately.
- Only available if your feeder is online (connected to PetKit's servers). | +| `Desiccant days remaining` | `Sensor` | Number of days left before the desiccant needs to be replaced. | +| `Food level` | `Binary Sensor` | Allows for determining if there is a food shortage. | +| `Child lock` | `Switch` | Only available if your feeder is online (connected to PetKit's servers). | +| `Dispense tone` | `Switch` | Only available if your feeder is online (connected to PetKit's servers). | +| `Food shortage alarm` | `Switch` | Only available if your feeder is online (connected to PetKit's servers). | +| `Reset desiccant` | `Button` | - Allows you to reset the desiccant back to 30 days after replacing it.
- Only available if your feeder is online (connected to PetKit's servers). | +| `Battery installed` | `Binary Sensor` | If batteries are removed or installed, power cycling the feeder is required for the status to update. | +| `Battery status` | `Sensor` | - Will only become available when feeder is running on batteries.
- Indicates the battery level (Normal or Low). | +| `Dispensed` | `Sensor` | Amount of food, in grams, dispensed today. | +| `Manually dispensed` | `Sensor` | Amount of food, in grams, manually dispensed today. | +| `Planned` | `Sensor` | Amount of food, in grams, that the feeder plans to dispense today. | +| `Planned dispensed` | `Sensor` | Of the planned amount that is to be dispensed today, amount of food (grams) that has been dispensed. | +| `RSSI` | `Sensor` | WiFi connection strength. | +| `Status` | `Sensor` | `Normal` = Feeder is connected to PetKit's servers
`Offline` = Feeder is not connected to PetKit servers
`On Batteries` = If installed, feeder is currently being powered by the batteries. | +| `Times dispensed` | `Sensor` | Number of times food has been dispensed today. | +
+ +
+ Fresh Element Mini Pro (click to expand) + + +Each Feeder has the following entities: + +| Entity | Entity Type | Additional Comments | +|-----------------------------|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Indicator light` | `Switch` | Only available if your feeder is online (connected to PetKit's servers). | +| `Manual feed` | `Select` | - Allows setting the amount of food to dispense immediately.
- Only available if your feeder is online (connected to PetKit's servers). | +| `Desiccant days remaining` | `Sensor` | Number of days left before the desiccant needs to be replaced. | +| `Food level` | `Binary Sensor` | Allows for determining if there is a food shortage. | +| `Child lock` | `Switch` | Only available if your feeder is online (connected to PetKit's servers). | +| `Reset desiccant` | `Button` | - Allows you to reset the desiccant back to 30 days after replacing it.
- Only available if your feeder is online (connected to PetKit's servers). | +| `Battery status` | `Sensor` | - Will only become available when feeder is running on batteries.
- Indicates the battery level (Normal or Low). | +| `RSSI` | `Sensor` | WiFi connection strength. | +| `Status` | `Sensor` | `Normal` = Feeder is connected to PetKit's servers
`Offline` = Feeder is not connected to PetKit servers
`On Batteries` = If installed, feeder is currently being powered by the batteries. | + +> Friendly Note: Mini feeders have a bug when batteries are installed that has never been addressed by PetKit. This can result in your device locking up until the batteries are removed and feeder is power cycled. I am not sure what triggers this bug, but as a safety measure I don't install batteries into mini feeders. +
+ +## Water Fountains +___ + +
+ Eversweet 3 Pro (click to expand) + Each water fountain has the following entities: -| Entity | Entity Type | Additional Comments | -|----------------------------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `Do not disturb` | `Switch` | -`Only available if BLE relay is set up and main BLE relay device is online.`
- Allows for enabling or disabling do not disturb - Use PetKit app to set do not disturb schedule if you plan on using this entity. | -| `Light` | `Switch` | `Only available if BLE relay is set up and main BLE relay device is online.` | -| `Power` | `Switch` | -`Only available if BLE relay is set up and main BLE relay device is online.`
- Turning power off is equivalent to "Pause" in the PetKit app.
- Turning power on resumes the water fountain in the mode (Normal/Smart) it was in prior to being powered off. | -| `Filter` | `Sensor` | Indicates % filter life left. | -| `Water level` | `Binary Sensor` | Indicates if water needs to be added to the water fountain. | -| `Light brightness` | `Select` | `Only available if BLE relay is set up and main BLE relay device is online.` | -| `Mode` | `Select` | -`Only available if BLE relay is set up and main BLE relay device is online.`
- Allows setting mode to Normal or Smart.
- For "Smart mode", use the PetKit app to set the water on and off minutes. | -| `Reset filter` | `Button` | `Only available if BLE relay is set up and main BLE relay device is online.` | -| `Energy usage` | `Sensor` | | -| `Last data update` | `Sensor` | - Date/Time that the water fountain last reported updated data to PetKit servers.
- This can be used to track and identify if the BLE relay is working correctly as this will change whenever the main BLE relay device polls the water fountain.
- If you have the BLE relay set up, this sensor is only concerning if it shows data was updated hours ago. | +| Entity | Entity Type | Additional Comments | +|------------------------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Do not disturb` | `Switch` | -`Only available if BLE relay is set up and main BLE relay device is online.`
- Allows for enabling or disabling do not disturb - Use PetKit app to set do not disturb schedule if you plan on using this entity. | +| `Light` | `Switch` | `Only available if BLE relay is set up and main BLE relay device is online.` | +| `Power` | `Switch` | -`Only available if BLE relay is set up and main BLE relay device is online.`
- Turning power off is equivalent to "Pause" in the PetKit app.
- Turning power on resumes the water fountain in the mode (Normal/Smart) it was in prior to being powered off. | +| `Filter` | `Sensor` | Indicates % filter life left. | +| `Water level` | `Binary Sensor` | Indicates if water needs to be added to the water fountain. | +| `Light brightness` | `Select` | -`Only available if BLE relay is set up and main BLE relay device is online.`
- Only available when light is turned on | +| `Mode` | `Select` | -`Only available if BLE relay is set up and main BLE relay device is online.`
- Allows setting mode to Normal or Smart.
- For "Smart mode", use the PetKit app to set the water on and off minutes. | +| `Reset filter` | `Button` | `Only available if BLE relay is set up and main BLE relay device is online.` | +| `Energy usage` | `Sensor` | | +| `Last data update` | `Sensor` | - Date/Time that the water fountain last reported updated data to PetKit servers.
- This can be used to track and identify if the BLE relay is working correctly as this will change whenever the main BLE relay device polls the water fountain.
- If you have the BLE relay set up, this sensor is only concerning if it shows data was updated hours ago. | +| `Purified water today` | `Sensor` | Number of times water has been purified. | **Note About Water Fountain Control** > If you have the BLE relay set up in the PetKit app and the main BLE relay device is online, sometimes the bluetooth connection will fail when attempting to send a command (e.g., changing mode, light brightness, etc) . If this happens, it will be noted in the Home Assistant logs. Retrying the command usually results in a subsequent successful connection. +
+ +## Litter Boxes +___ + +
+ Pura X (click to expand) + + +Each litter box has the following entities: + +| Entity | Entity Type | Additional Comments | +|------------------------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Odor removal` | `Button` | - Activates immediate odor removal
- Only available if litter box is online (Connected to PetKit's servers). | +| `Pause Cleaning` | `Button` | Only available if litter box is online (Connected to PetKit's servers). | +| `Power` | `Button` | - Turn litter box on/off
- Only available if litter box is online (Connected to PetKit's servers). | +| `Start/Resume cleaning` | `Button` | - Start a manual cleaning or resume a cleaning if litter box is currently paused.
- Only available if litter box is online (Connected to PetKit's servers). | +| `Average Use` | `Sensor` | Average use duration of the litter box today. | +| `Deodorizer` | `Binary Sensor` | Allows for determining if deodorizer needs to be refilled. | +| `Last used by` | `Sensor` | Indicates which cat used the litter box last. | +| `Litter` | `Binary Sensor` | Allows for determining if the litter needs to be refilled. | +| `Times used` | `Sensor` | Number of times litter box was used today. | +| `Total use` | `Sensor` | Total number of seconds litter box was used today. | +| `Waste bin` | `Binary Sensor` | Allows for determining if the waste bin is full. | +| `Auto cleaning` | `Switch` | - Only available if litter box is online (Connected to PetKit's servers).
- Not available if Kitten Mode is turned on. | +| `Auto odor removal` | `Switch` | Only available if litter box is online (Connected to PetKit's servers). | +| `Avoid repeat cleaning` | `Switch` | - Only available if litter box is online (Connected to PetKit's servers).
- Not available if Auto Cleaning is disabled.
- Not available if Kitten Mode is turned on. | +| `Child lock`| `Switch` | Only available if litter box is online (Connected to PetKit's servers). | +| `Cleaning delay` | `Number` | - Only available if litter box is online (Connected to PetKit's servers).
- Not available if Auto Cleaning is disabled.
- Not available if Kitten Mode is turned on. | +| `Cleaning interval` | `Select` | - Only available if litter box is online (Connected to PetKit's servers).
- Not available if Auto Cleaning is disabled.
- Not available if Avoid repeat cleaning is disabled.
- Not available if Kitten Mode is turned on. | +| `Display` | `Switch` | - Turn display on litter box on or off.
- Only available if litter box is online (Connected to PetKit's servers). | +| `Do not disturb` | `Switch` | Only available if litter box is online (Connected to PetKit's servers). | +| `Kitten mode` | `Switch` | Only available if litter box is online (Connected to PetKit's servers). | +| `Light weight cleaning disabled` | `Switch` | - Only available if litter box is online (Connected to PetKit's servers).
- Not available if Auto Cleaning is disabled.
- Not available if Avoid repeat cleaning is disabled.
- Not available if Kitten Mode is turned on. | +| `Litter type` | `Select` | Type of litter that is being used in the litter box. | +| `Periodic cleaning` | `Switch` | - Only available if litter box is online (Connected to PetKit's servers).
- Not available if Kitten Mode is turned on. | +| `Periodic odor removal` | `Switch` | Only available if litter box is online (Connected to PetKit's servers). | +| `Deodorizer level` | `Sensor` | Percent of deodorizer remaining. | +| `Error` | `Sensor` | Any errors being reported by the litter box. | +| `Last event` | `Sensor` | - Last event that occured in the litter box.
- sub_events attribute is used to list any events that are associated with the main event.
- This sensor is used to mimic the timeline that is seen in the PetKit app. | +| `Litter level` | `Sensor` | Percent of litter that is left in the litter box. | +| `Litter weight` | `Sensor` | - Weight of litter currently in litter box.
- By default this is in Kg. The unit can be changed in the settings of this entity. | +| `Manually paused` | `Binary Sensor` | Indicates if the litter box has been manually paused or not. Please see note below table for additional information. | +| `RSSI` | `Sensor` | WiFi connection strength. | + +> When manually pausing the litter box, the manually paused sensor entity will show a state of On. If cleaning isn't resumed, the litter box will (by default) resume cleaning after 10 minutes - the manually paused entity will return to a state of Off. If you manually pause a cleaning and restart Home Assistant while the litter box is paused, this sensor will have an incorrect state of Off. This is a limitation on PetKit's end as the state of the litter box is not reported by their servers. This behavior is evident when a cleaning is paused from the PetKit app, the app is force closed, and opened again. The goal of this entity is to be able to manually keep track of the state of the litter box since PetKit doesn't do that. +
+ +## Pets +___ + +
+ Dog (click to expand) + + +| Entity | Entity Type | Additional Comments | +|------------------------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Set weight` | `Number` | Set the weight of the dog. | + +
+ +
+ Cat (click to expand) + + +| Entity | Entity Type | Additional Comments | +|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Set weight` | `Number` | Set the weight of the cat. | +| `Last use duration` | `Sensor` | - Amount of time spent in the litter box during last use today.
- Only available if PetKit account has litter box(es).
- If multiple litter boxes, this will display data obtained from the most recent litter box used. | +| `Latest weight` | `Sensor` | - Most recent weight measurement obtained during last litter box use today.
- Only available if PetKit account has litter box(es).
- If multiple litter boxes, this will display data obtained from the most recent litter box used.
- By default the unit used is Kg. Unit can be changed in the settings of the entity. | +
From 60636c9a17523812fbca223c4a4495a8c0118d05 Mon Sep 17 00:00:00 2001 From: Robert Drinovac <52541649+RobertD502@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:43:46 -0500 Subject: [PATCH 02/11] bump to version 0.1.0b1, bump petkitaio, add tzlocal requirement --- custom_components/petkit/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/petkit/manifest.json b/custom_components/petkit/manifest.json index 96a37c5..7561630 100644 --- a/custom_components/petkit/manifest.json +++ b/custom_components/petkit/manifest.json @@ -1,13 +1,13 @@ { "domain": "petkit", "name": "PetKit", - "version": "0.1.0b0", + "version": "0.1.0b1", "integration_type": "hub", "iot_class": "cloud_polling", "documentation": "https://github.com/RobertD502/home-assistant-petkit/blob/main/README.md", "issue_tracker": "https://github.com/RobertD502/home-assistant-petkit/issues", "config_flow": true, - "requirements": ["petkitaio==0.1.0"], + "requirements": ["petkitaio==0.1.1", "tzlocal>=4.2"], "dependencies": [], "codeowners": ["@RobertD502"] } From e7a57a7ce56b7b1048f9aac3948ec6bb68b57d51 Mon Sep 17 00:00:00 2001 From: Robert Drinovac <52541649+RobertD502@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:45:35 -0500 Subject: [PATCH 03/11] add support for infinity feeder and pura x --- custom_components/petkit/binary_sensor.py | 369 +++++++++++++++++++++- 1 file changed, 359 insertions(+), 10 deletions(-) diff --git a/custom_components/petkit/binary_sensor.py b/custom_components/petkit/binary_sensor.py index df0ba5a..f93cfd9 100644 --- a/custom_components/petkit/binary_sensor.py +++ b/custom_components/petkit/binary_sensor.py @@ -3,7 +3,7 @@ from typing import Any -from petkitaio.model import Feeder, W5Fountain +from petkitaio.model import Feeder, LitterBox, W5Fountain from homeassistant.components.binary_sensor import BinarySensorDeviceClass, BinarySensorEntity from homeassistant.config_entries import ConfigEntry @@ -12,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, FEEDERS, WATER_FOUNTAINS +from .const import DOMAIN, FEEDERS, LITTER_BOXES, WATER_FOUNTAINS from .coordinator import PetKitDataUpdateCoordinator @@ -43,6 +43,22 @@ async def async_setup_entry( BatteryInstalled(coordinator, feeder_id), )) + # D3 Feeder + if feeder_data.type == 'd3': + binary_sensors.extend(( + BatteryCharging(coordinator, feeder_id), + )) + + # Litter boxes + for lb_id, lb_data in coordinator.data.litter_boxes.items(): + # Pura X + binary_sensors.extend(( + LBBinFull(coordinator, lb_id), + LBLitterLack(coordinator, lb_id), + LBDeodorizerLack(coordinator, lb_id), + LBManuallyPaused(coordinator, lb_id), + )) + async_add_entities(binary_sensors) @@ -166,19 +182,33 @@ def device_class(self) -> BinarySensorDeviceClass: def is_on(self) -> bool: """Return True if food needs to be added.""" - if self.feeder_data.data['state']['food'] == 0: - return True - else: - return False + if self.feeder_data.type == 'd3': + if self.feeder_data.data['state']['food'] < 2: + return True + else: + return False + + if self.feeder_data.type != 'd3': + if self.feeder_data.data['state']['food'] == 0: + return True + else: + return False @property def icon(self) -> str: """Set icon.""" - if self.feeder_data.data['state']['food'] == 0: - return 'mdi:food-drumstick-off' - else: - return 'mdi:food-drumstick' + if self.feeder_data.type == 'd3': + if self.feeder_data.data['state']['food'] < 2: + return 'mdi:food-drumstick-off' + else: + return 'mdi:food-drumstick' + + if self.feeder_data.type != 'd3': + if self.feeder_data.data['state']['food'] == 0: + return 'mdi:food-drumstick-off' + else: + return 'mdi:food-drumstick' class BatteryInstalled(CoordinatorEntity, BinarySensorEntity): """Representation of if Feeder has batteries installed.""" @@ -243,3 +273,322 @@ def icon(self) -> str: """Set icon.""" return 'mdi:battery' + + +class BatteryCharging(CoordinatorEntity, BinarySensorEntity): + """Representation of if Feeder battery is charging.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_battery_charging' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Battery" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + + @property + def device_class(self) -> BinarySensorDeviceClass: + """Return entity device class.""" + + return BinarySensorDeviceClass.BATTERY_CHARGING + + @property + def is_on(self) -> bool: + """Return True if battery is charging.""" + + if self.feeder_data.data['state']['charge'] > 1: + return True + else: + return False + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:battery' + + +class LBBinFull(CoordinatorEntity, BinarySensorEntity): + """Representation of litter box wastebin full or not.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_wastebin' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Wastebin" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:delete' + + @property + def device_class(self) -> BinarySensorDeviceClass: + """Return entity device class.""" + + return BinarySensorDeviceClass.PROBLEM + + @property + def is_on(self) -> bool: + """Return True if wastebin is full.""" + + return self.lb_data.device_detail['state']['boxFull'] + + +class LBLitterLack(CoordinatorEntity, BinarySensorEntity): + """Representation of litter box lacking sand.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_litter_lack' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Litter" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:landslide' + + @property + def device_class(self) -> BinarySensorDeviceClass: + """Return entity device class.""" + + return BinarySensorDeviceClass.PROBLEM + + @property + def is_on(self) -> bool: + """Return True if litter is empty.""" + + return self.lb_data.device_detail['state']['sandLack'] + + +class LBDeodorizerLack(CoordinatorEntity, BinarySensorEntity): + """Representation of litter box lacking deodorizer.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_deodorizer_lack' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Deodorizer" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:spray' + + @property + def device_class(self) -> BinarySensorDeviceClass: + """Return entity device class.""" + + return BinarySensorDeviceClass.PROBLEM + + @property + def is_on(self) -> bool: + """Return True if deodorizer is empty.""" + + return self.lb_data.device_detail['state']['liquidLack'] + + +class LBManuallyPaused(CoordinatorEntity, BinarySensorEntity): + """Representation of if litter box is manually paused by user.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_manually_paused' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Manually paused" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:pause' + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + + @property + def is_on(self) -> bool: + """Return True if deodorizer is empty.""" + + return self.lb_data.manually_paused From 151bac7dad508f76d7e82964a17dfe901ff86c59 Mon Sep 17 00:00:00 2001 From: Robert Drinovac <52541649+RobertD502@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:46:28 -0500 Subject: [PATCH 04/11] add support for infinity feeder and pura x --- custom_components/petkit/button.py | 359 ++++++++++++++++++++++++++++- 1 file changed, 351 insertions(+), 8 deletions(-) diff --git a/custom_components/petkit/button.py b/custom_components/petkit/button.py index 9a0739b..558b637 100644 --- a/custom_components/petkit/button.py +++ b/custom_components/petkit/button.py @@ -4,8 +4,8 @@ from typing import Any import asyncio -from petkitaio.constants import W5Command -from petkitaio.model import Feeder, W5Fountain +from petkitaio.constants import LitterBoxCommand, W5Command +from petkitaio.model import Feeder, LitterBox, W5Fountain from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry @@ -14,7 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, FEEDERS, WATER_FOUNTAINS +from .const import DOMAIN, FEEDERS, LITTER_BOXES, WATER_FOUNTAINS from .coordinator import PetKitDataUpdateCoordinator @@ -40,11 +40,27 @@ async def async_setup_entry( ResetDesiccant(coordinator, feeder_id), )) - if feeder_data.type == 'd4': + # D3 and D4 + if feeder_data.type in ['d3', 'd4']: buttons.extend(( CancelManualFeed(coordinator, feeder_id), )) + # D3 + if feeder_data.type == 'd3': + buttons.extend(( + CallPet(coordinator, feeder_id), + )) + + # Litter boxes + for lb_id, lb_data in coordinator.data.litter_boxes.items(): + # Pura X + buttons.extend(( + LBStartCleaning(coordinator, lb_id), + LBPauseCleaning(coordinator, lb_id), + LBOdorRemoval(coordinator, lb_id), + )) + async_add_entities(buttons) @@ -123,6 +139,7 @@ async def async_press(self) -> None: await asyncio.sleep(1) await self.coordinator.async_request_refresh() + class ResetDesiccant(CoordinatorEntity, ButtonEntity): """Representation of feeder desiccant reset button.""" @@ -190,6 +207,7 @@ async def async_press(self) -> None: self.async_write_ha_state() await self.coordinator.async_request_refresh() + class CancelManualFeed(CoordinatorEntity, ButtonEntity): """Representation of manual feed cancelation button.""" @@ -242,14 +260,339 @@ def available(self) -> bool: else: return False + async def async_press(self) -> None: + """Handle the button press.""" + + await self.coordinator.client.cancel_manual_feed(self.feeder_data) + await self.coordinator.async_request_refresh() + + +class CallPet(CoordinatorEntity, ButtonEntity): + """Representation of calling pet button for d3 feeder.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + @property - def entity_category(self) -> EntityCategory: - """Set category to config.""" + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" - return EntityCategory.CONFIG + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_call_pet' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Call pet" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if self.feeder_data.data['state']['pim'] != 0: + return True + else: + return False async def async_press(self) -> None: """Handle the button press.""" - await self.coordinator.client.cancel_manual_feed(self.feeder_data) + await self.coordinator.client.call_pet(self.feeder_data) + await self.coordinator.async_request_refresh() + + +class LBStartCleaning(CoordinatorEntity, ButtonEntity): + """Representation of litter box start/resume cleaning.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_start_cleaning' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Start/Resume cleaning" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:vacuum' + + @property + def available(self) -> bool: + """Only make available if device is online and on.""" + + lb_online = self.lb_data.device_detail['state']['pim'] == 1 + lb_power_on = self.lb_data.device_detail['state']['power'] == 1 + + if (lb_online and lb_power_on): + return True + else: + return False + + async def async_press(self) -> None: + """Handle the button press.""" + + await self.coordinator.client.control_litter_box(self.lb_data, LitterBoxCommand.STARTCLEAN) + await self.coordinator.async_request_refresh() + + +class LBPauseCleaning(CoordinatorEntity, ButtonEntity): + """Representation of litter box pause cleaning.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_pause_cleaning' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Pause cleaning" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:pause' + + @property + def available(self) -> bool: + """Only make available if device is online and on.""" + + lb_online = self.lb_data.device_detail['state']['pim'] == 1 + lb_power_on = self.lb_data.device_detail['state']['power'] == 1 + + if (lb_online and lb_power_on): + return True + else: + return False + + async def async_press(self) -> None: + """Handle the button press.""" + + await self.coordinator.client.control_litter_box(self.lb_data, LitterBoxCommand.PAUSECLEAN) + await self.coordinator.async_request_refresh() + + +class LBOdorRemoval(CoordinatorEntity, ButtonEntity): + """Representation of litter box odor removal.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_odor_removal' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Odor removal" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:scent' + + @property + def available(self) -> bool: + """Only make available if device is online and on.""" + + lb_online = self.lb_data.device_detail['state']['pim'] == 1 + lb_power_on = self.lb_data.device_detail['state']['power'] == 1 + + if (lb_online and lb_power_on): + return True + else: + return False + + async def async_press(self) -> None: + """Handle the button press.""" + + await self.coordinator.client.control_litter_box(self.lb_data, LitterBoxCommand.ODORREMOVAL) + await self.coordinator.async_request_refresh() + + +class LBResetDeodorizer(CoordinatorEntity, ButtonEntity): + """Representation of litter box deodorizer reset.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_reset_deodorizer' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Reset deodorizer" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:scent' + + @property + def available(self) -> bool: + """Only make available if device is online and on.""" + + lb_online = self.lb_data.device_detail['state']['pim'] == 1 + lb_power_on = self.lb_data.device_detail['state']['power'] == 1 + + if (lb_online and lb_power_on): + return True + else: + return False + + async def async_press(self) -> None: + """Handle the button press.""" + + await self.coordinator.client.control_litter_box(self.lb_data, LitterBoxCommand.RESETDEODOR) await self.coordinator.async_request_refresh() From e651b48708c2bae5def68aa223f989d38edd23c5 Mon Sep 17 00:00:00 2001 From: Robert Drinovac <52541649+RobertD502@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:47:42 -0500 Subject: [PATCH 05/11] add pura x and infinity feeder constants --- custom_components/petkit/const.py | 128 ++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/custom_components/petkit/const.py b/custom_components/petkit/const.py index 375efc0..d86b9ae 100644 --- a/custom_components/petkit/const.py +++ b/custom_components/petkit/const.py @@ -17,6 +17,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH, @@ -65,10 +66,30 @@ } FEEDERS = { + 'd3': 'Fresh Element Infinity', 'd4': 'Fresh Element Solo', 'feedermini': 'Fresh Element Mini Pro', } +LITTER_BOXES = { + 't3': 'PURA X', + 't4': 'PURA MAX', +} + +CLEANING_INTERVAL_NAMED = { + 0: 'Disabled', + 300: '5min', + 600: '10min', + 900: '15min', + 1800: '30min', + 2700: '45min', + 3600: '1h', + 4500: '1h15min', + 5400: '1h30min', + 6300: '1h45min', + 7200: '2h' +} + FEEDER_MANUAL_FEED_OPTIONS = ['', '1/10th Cup (10g)', '1/5th Cup (20g)', '3/10th Cup (30g)', '2/5th Cup (40g)', '1/2 Cup (50g)'] MINI_FEEDER_MANUAL_FEED_OPTIONS = [ '', @@ -84,6 +105,12 @@ '1/2 Cup (50g)' ] +LITTER_TYPE_NAMED = { + 1: 'Bentonite', + 2: 'Tofu', + 3: 'Mixed' +} + MANUAL_FEED_NAMED = { 0: '', 5: '1/20th Cup (5g)', @@ -97,3 +124,104 @@ 45: '9/20th Cup (45g)', 50: '1/2 Cup (50g)' } + +VALID_EVENT_TYPES = [5, 6, 7, 8, 10] +EVENT_TYPE_NAMED = { + 5: 'Cleaning Completed', + 6: 'Dumping Over', + 7: 'Reset Over', + 8: 'Spray Over', + 10: 'Pet Out', +} + +# Event Type --> Result --> Reason --> Optional(Error) +EVENT_DESCRIPTION = { + 5: { + 0: { + 0: 'Auto cleaning completed', + 1: 'Periodic cleaning completed', + 2: 'Manual cleaning completed', + 3: 'Manual cleaning completed', + }, + 1: { + 0: 'Automatic cleaning terminated', + 1: 'Periodic cleaning terminated', + 2: 'Manual cleaning terminated', + 3: 'Manual cleaning terminated', + }, + 2: { + 0: { + 'full': 'Automatic cleaning failed, waste collection bin is full, please empty promptly', + 'hallL': 'Automatic cleaning failure, the cylinder is not properly locked in place, please check', + 'hallT': 'Automatic cleaning failure, the litter box\'s upper cupper cover is not placed properly, please check', + }, + 1: { + 'full': 'Scheduled cleaning failed, waste collection bin is full, please empty promptly', + 'hallL': 'Scheduled cleaning failure, the cylinder is not properly locked in place, please check', + 'hallT': 'Scheduled cleaning failure, the litter box\'s upper cupper cover is not placed properly, please check', + }, + 2: { + 'full': 'Manual cleaning failed, waste collection bin is full, please empty promptly', + 'hallL': 'Manual cleaning failure, the cylinder is not properly locked in place, please check', + 'hallT': 'Manual cleaning failure, the litter box\'s upper cupper cover is not placed properly, please check', + }, + 3: { + 'full': 'Manual cleaning failed, waste collection bin is full, please empty promptly', + 'hallL': 'Manual cleaning failure, the cylinder is not properly locked in place, please check', + 'hallT': 'Manual cleaning failure, the litter box\'s upper cupper cover is not placed properly, please check', + }, + }, + 3: { + 0: 'Automatic cleaning cancelled, device in operation', + 1: 'Periodic cleaning cancelled, device in operation', + 2: 'Manual cleaning cancelled, device in operation', + 3: 'Manual cleaning cancelled, device in operation', + }, + 4: { + 0: 'Kitten mode is enabled, auto cleaning is canceled', + 1: 'Kitten mode is enabled, periodically cleaning is canceled', + }, + }, + 6: { + 0: 'Cat litter empty completed', + 1: 'Cat litter empty terminated', + 2: { + 'full': 'Cat litter empty failed, waste collection bin is full, please empty promptly', + 'hallL': 'Cat litter empty failure, the cylinder is not properly locked in place, please check', + 'hallT': 'Cat litter empty failure, the litter box\'s cupper cover is not placed properly, please check', + }, + }, + 7: { + 0: 'Device reset completed', + 1: 'Device reset terminated', + 2: { + 'full': 'Device reset failed, waste collection bin is full, please empty promptly', + 'hallL': 'Device reset failure, the cylinder is not properly locked in place, please check', + 'hallT': 'Device reset failure, the litter box\'s cupper cover is not placed properly, please check', + }, + }, + 8: { + 0: { + 0: 'Deodorant finished', + 1: 'Periodic odor removal completed', + 2: 'Manual odor removal completed', + 3: 'Manual odor removal completed', + }, + 1: { + 0: 'Deodorant finished, not enough purifying liquid, please refill in time', + 1: 'Periodic odor removal completed, not enough purifying liquid, please refill in time', + 2: 'Manual odor removal completed, not enough purifying liquid, please refill in time', + 3: 'Manual odor removal completed, not enough purifying liquid, please refill in time', + }, + 2: { + 0: 'Automatic odor removal failed, odor eliminator error', + 1: 'Periodic odor removal failure, odor eliminator malfunction', + 2: 'Manual odor removal failure, odor eliminator malfunction', + 3: 'Manual odor removal failure, odor eliminator malfunction', + }, + }, +} + + + + From 6a434483629671289859aca5854aba764cc4f0c1 Mon Sep 17 00:00:00 2001 From: Robert Drinovac <52541649+RobertD502@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:49:49 -0500 Subject: [PATCH 06/11] add number entities for pura x and infinity feeder --- custom_components/petkit/number.py | 630 +++++++++++++++++++++++++++++ 1 file changed, 630 insertions(+) create mode 100644 custom_components/petkit/number.py diff --git a/custom_components/petkit/number.py b/custom_components/petkit/number.py new file mode 100644 index 0000000..d4276dc --- /dev/null +++ b/custom_components/petkit/number.py @@ -0,0 +1,630 @@ +"""Number platform for PetKit integration.""" +from __future__ import annotations + +from typing import Any + +from petkitaio.constants import FeederSetting, LitterBoxSetting, PetSetting +from petkitaio.exceptions import PetKitError +from petkitaio.model import Feeder, LitterBox, Pet + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfMass, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.unit_system import METRIC_SYSTEM + +from .const import DOMAIN, FEEDERS, LITTER_BOXES +from .coordinator import PetKitDataUpdateCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set Up PetKit Number Entities.""" + + coordinator: PetKitDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + numbers = [] + + # Pets + for pet_id, pet_data in coordinator.data.pets.items(): + numbers.extend(( + PetWeight(coordinator, pet_id), + )) + + for feeder_id, feeder_data in coordinator.data.feeders.items(): + # Only D3 Feeder + if feeder_data.type == 'd3': + numbers.extend(( + Surplus(coordinator, feeder_id), + Volume(coordinator, feeder_id), + ManualFeed(coordinator, feeder_id), + )) + + # Litter boxes + for lb_id, lb_data in coordinator.data.litter_boxes.items(): + # Pura X + numbers.extend(( + LBCleaningDelay(coordinator, lb_id), + )) + + async_add_entities(numbers) + + +class PetWeight(CoordinatorEntity, NumberEntity): + """Representation of Pet Weight.""" + + def __init__(self, coordinator, pet_id): + super().__init__(coordinator) + self.pet_id = pet_id + + @property + def pet_data(self) -> Pet: + """Handle coordinator Pet data.""" + + return self.coordinator.data.pets[self.pet_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.pet_data.id)}, + "name": self.pet_data.data['name'], + "manufacturer": "PetKit", + "model": self.pet_data.type, + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return self.pet_data.id + '_set_weight' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Set weight" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def entity_picture(self) -> str: + """Grab associated pet picture.""" + + if 'avatar' in self.pet_data.data: + return self.pet_data.data['avatar'] + else: + return None + + @property + def icon(self) -> str: + """Set icon if the pet doesn't have an avatar.""" + + if 'avatar' in self.pet_data.data: + return None + else: + return 'mdi:weight' + + @property + def native_value(self) -> float: + """Returns current weight.""" + + pet_weight = self.pet_data.data['weight'] + if self.hass.config.units is METRIC_SYSTEM: + return pet_weight + else: + return round((pet_weight * 2.2046226), 1) + + @property + def native_unit_of_measurement(self) -> UnitOfMass: + """Return kilograms or pounds.""" + + if self.hass.config.units is METRIC_SYSTEM: + return UnitOfMass.KILOGRAMS + else: + return UnitOfMass.POUNDS + + @property + def device_class(self) -> NumberDeviceClass: + """Return weight device class.""" + + return NumberDeviceClass.WEIGHT + + @property + def mode(self) -> NumberMode: + """Return box mode.""" + + return NumberMode.BOX + + @property + def native_min_value(self) -> float: + """Return minimum allowed value.""" + + if self.hass.config.units is METRIC_SYSTEM: + return 1.0 + else: + return 2.2 + + @property + def native_max_value(self) -> float: + """Return max value allowed.""" + + if self.hass.config.units is METRIC_SYSTEM: + return 150.0 + else: + return 330.0 + + @property + def native_step(self) -> int: + """Return stepping by 10 grams.""" + + return 0.1 + + async def async_set_native_value(self, value: int) -> None: + """Update the current value.""" + + if self.hass.config.units is METRIC_SYSTEM: + # Always send value with one decimal point in case user sends more decimal points or none + converted_value = round(value, 1) + else: + converted_value = round((value * 0.4535924), 1) + await self.coordinator.client.update_pet_settings(self.pet_data, PetSetting.WEIGHT, converted_value) + await self.coordinator.async_request_refresh() + + +class Surplus(CoordinatorEntity, NumberEntity): + """Representation of D3 Feeder surplus amount.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_surplus' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Surplus" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:food-drumstick' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def native_value(self) -> int: + """Returns current surplus setting.""" + + return self.feeder_data.data['settings']['surplus'] + + @property + def native_unit_of_measurement(self) -> UnitOfMass: + """Return grams.""" + + return UnitOfMass.GRAMS + + @property + def device_class(self) -> NumberDeviceClass: + """Return weight device class.""" + + return NumberDeviceClass.WEIGHT + + @property + def mode(self) -> NumberMode: + """Return slider mode.""" + + return NumberMode.SLIDER + + @property + def native_min_value(self) -> int: + """Return minimum allowed value.""" + + return 20 + + @property + def native_max_value(self) -> int: + """Return max value allowed.""" + + return 100 + + @property + def native_step(self) -> int: + """Return stepping by 10 grams.""" + + return 10 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if self.feeder_data.data['state']['pim'] != 0: + return True + else: + return False + + async def async_set_native_value(self, value: int) -> None: + """Update the current value.""" + + await self.coordinator.client.update_feeder_settings(self.feeder_data, FeederSetting.SURPLUS, int(value)) + self.feeder_data.data['settings']['surplus'] = value + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class Volume(CoordinatorEntity, NumberEntity): + """Representation of D3 Feeder speaker volume.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_volume' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Volume" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:volume-high' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def native_value(self) -> int: + """Returns current volume setting.""" + + return self.feeder_data.data['settings']['volume'] + + @property + def mode(self) -> NumberMode: + """Return slider mode.""" + + return NumberMode.SLIDER + + @property + def native_min_value(self) -> int: + """Return minimum allowed value.""" + + return 1 + + @property + def native_max_value(self) -> int: + """Return max value allowed.""" + + return 9 + + @property + def native_step(self) -> int: + """Return stepping by 1.""" + + return 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if self.feeder_data.data['state']['pim'] != 0: + return True + else: + return False + + async def async_set_native_value(self, value: int) -> None: + """Update the current value.""" + + await self.coordinator.client.update_feeder_settings(self.feeder_data, FeederSetting.VOLUME, int(value)) + self.feeder_data.data['settings']['volume'] = value + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class ManualFeed(CoordinatorEntity, NumberEntity): + """Representation of D3 Feeder manual feeding.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_manual_feed' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Manual feed" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:bowl-mix' + + @property + def native_value(self) -> int: + """Returns lowest amount allowed.""" + + return 4 + + @property + def native_unit_of_measurement(self) -> UnitOfMass: + """Return grams.""" + + return UnitOfMass.GRAMS + + @property + def device_class(self) -> NumberDeviceClass: + """Return weight device class.""" + + return NumberDeviceClass.WEIGHT + + @property + def mode(self) -> NumberMode: + """Return slider mode.""" + + return NumberMode.SLIDER + + @property + def native_min_value(self) -> int: + """Return minimum allowed value.""" + + return 4 + + @property + def native_max_value(self) -> int: + """Return max value allowed.""" + + return 200 + + @property + def native_step(self) -> int: + """Return stepping by 1.""" + + return 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if self.feeder_data.data['state']['pim'] != 0: + return True + else: + return False + + async def async_set_native_value(self, value: int) -> None: + """Update the current value.""" + + if (value < 5) or (value > 200): + raise PetKitError(f'{self.feeder_data.data["name"]} can only accept manual feeding amounts between 5 to 200 grams') + else: + await self.coordinator.client.manual_feeding(self.feeder_data, int(value)) + await self.coordinator.async_request_refresh() + + +class LBCleaningDelay(CoordinatorEntity, NumberEntity): + """Representation of litter box cleaning delay.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_cleaning_delay' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Cleaning delay" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:motion-pause' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def native_value(self) -> int: + """Returns currently set delay in minutes.""" + + return (self.lb_data.device_detail['settings']['stillTime'] / 60) + + @property + def native_unit_of_measurement(self) -> UnitOfMass: + """Return grams.""" + + return UnitOfTime.MINUTES + + @property + def mode(self) -> NumberMode: + """Return slider mode.""" + + return NumberMode.SLIDER + + @property + def native_min_value(self) -> int: + """Return minimum allowed value.""" + + return 0 + + @property + def native_max_value(self) -> int: + """Return max value allowed.""" + + return 60 + + @property + def native_step(self) -> int: + """Return stepping by 1.""" + + return 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + kitten_mode_off = self.lb_data.device_detail['settings']['kitten'] == 0 + auto_clean = self.lb_data.device_detail['settings']['autoWork'] == 1 + + if self.lb_data.device_detail['state']['pim'] != 0: + # Only available if kitten mode off and auto cleaning on + if (kitten_mode_off and auto_clean): + return True + else: + return False + else: + return False + + async def async_set_native_value(self, value: int) -> None: + """Update the current value.""" + + seconds = int(value * 60) + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.DELAYCLEANTIME, seconds) + self.lb_data.device_detail['settings']['stillTime'] = seconds + self.async_write_ha_state() + await self.coordinator.async_request_refresh() From dc32c65b11c7fd6c014a7df379097ce006c2668d Mon Sep 17 00:00:00 2001 From: Robert Drinovac <52541649+RobertD502@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:50:44 -0500 Subject: [PATCH 07/11] add infinity feeder and pura x support --- custom_components/petkit/select.py | 309 ++++++++++++++++++++++++++++- 1 file changed, 304 insertions(+), 5 deletions(-) diff --git a/custom_components/petkit/select.py b/custom_components/petkit/select.py index bd4d978..4a701ee 100644 --- a/custom_components/petkit/select.py +++ b/custom_components/petkit/select.py @@ -4,9 +4,9 @@ from typing import Any import asyncio -from petkitaio.constants import W5Command +from petkitaio.constants import FeederSetting, LitterBoxSetting, W5Command from petkitaio.exceptions import BluetoothError -from petkitaio.model import Feeder, W5Fountain +from petkitaio.model import Feeder, LitterBox, W5Fountain from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry @@ -16,12 +16,15 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import( + CLEANING_INTERVAL_NAMED, DOMAIN, FEEDERS, FEEDER_MANUAL_FEED_OPTIONS, LIGHT_BRIGHTNESS_COMMAND, LIGHT_BRIGHTNESS_NAMED, LIGHT_BRIGHTNESS_OPTIONS, + LITTER_BOXES, + LITTER_TYPE_NAMED, MANUAL_FEED_NAMED, MINI_FEEDER_MANUAL_FEED_OPTIONS, WATER_FOUNTAINS, @@ -37,6 +40,8 @@ WF_MODE_TO_PETKIT = {v: k for (k, v) in WF_MODE_COMMAND.items()} WF_MODE_TO_PETKIT_NUMBERED = {v: k for (k, v) in WF_MODE_NAMED.items()} MANUAL_FEED_TO_PETKIT = {v: k for (k, v) in MANUAL_FEED_NAMED.items()} +CLEANING_INTERVAL_TO_PETKIT = {v: k for (k, v) in CLEANING_INTERVAL_NAMED.items()} +LITTER_TYPE_TO_PETKIT = {v: k for (k, v) in LITTER_TYPE_NAMED.items()} async def async_setup_entry( @@ -56,9 +61,23 @@ async def async_setup_entry( WFMode(coordinator, wf_id), )) for feeder_id, feeder_data in coordinator.data.feeders.items(): - # All Feeders + # D4 and Mini Feeders + if feeder_data.type in ['d4', 'feedermini']: + selects.extend(( + ManualFeed(coordinator, feeder_id), + )) + # D3 Feeder + if feeder_data.type == 'd3': + selects.extend(( + Sound(coordinator, feeder_id), + )) + + # Litter boxes + for lb_id, lb_data in coordinator.data.litter_boxes.items(): + # Pura X selects.extend(( - ManualFeed(coordinator, feeder_id), + LBCleaningInterval(coordinator, lb_id), + LBLitterType(coordinator, lb_id), )) async_add_entities(selects) @@ -133,7 +152,7 @@ def available(self) -> bool: and the main relay device is online. """ - if self.wf_data.ble_relay: + if self.wf_data.ble_relay and (self.wf_data.data['settings']['lampRingSwitch'] == 1): return True else: return False @@ -345,3 +364,283 @@ async def async_select_option(self, option: str) -> None: await self.coordinator.client.manual_feeding(self.feeder_data, ha_to_petkit) await self.coordinator.async_request_refresh() + + +class Sound(CoordinatorEntity, SelectEntity): + """Representation of D3 Sound selection.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_sound' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Sound" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:surround-sound' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if self.feeder_data.data['state']['pim'] != 0: + return True + else: + return False + + @property + def current_option(self) -> str: + """Return currently selected sound.""" + + available_sounds = self.feeder_data.sound_list + current_sound_id = self.feeder_data.data['settings']['selectedSound'] + return available_sounds[current_sound_id] + + @property + def options(self) -> list[str]: + """Return list of all available sound names.""" + + available_sounds = self.feeder_data.sound_list + sound_names = list(available_sounds.values()) + return sound_names + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + + available_sounds = self.feeder_data.sound_list + NAME_TO_SOUND_ID = {v: k for (k, v) in available_sounds.items()} + ha_to_petkit = NAME_TO_SOUND_ID.get(option) + + await self.coordinator.client.update_feeder_settings(self.feeder_data, FeederSetting.SELECTEDSOUND, ha_to_petkit) + self.feeder_data.data['settings']['selectedSound'] = ha_to_petkit + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBCleaningInterval(CoordinatorEntity, SelectEntity): + """Representation of litter box cleaning interval.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_cleaning_interval' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Cleaning interval" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.lb_data.device_detail['settings']['autoIntervalMin'] == 0: + return 'mdi:timer-off' + else: + return 'mdi:timer' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + kitten_mode_off = self.lb_data.device_detail['settings']['kitten'] == 0 + auto_clean = self.lb_data.device_detail['settings']['autoWork'] == 1 + avoid_repeat = self.lb_data.device_detail['settings']['avoidRepeat'] == 1 + + if self.lb_data.device_detail['state']['pim'] != 0: + # Only available if kitten mode is off and auto clean and avoid repeat are on + if (kitten_mode_off and auto_clean and avoid_repeat): + return True + else: + return False + else: + return False + + @property + def current_option(self) -> str: + """Return currently selected interval.""" + + return CLEANING_INTERVAL_NAMED[self.lb_data.device_detail['settings']['autoIntervalMin']] + + @property + def options(self) -> list[str]: + """Return list of all available intervals.""" + + intervals = list(CLEANING_INTERVAL_NAMED.values()) + return intervals + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + + ha_to_petkit = CLEANING_INTERVAL_TO_PETKIT.get(option) + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.CLEANINTERVAL, ha_to_petkit) + self.lb_data.device_detail['settings']['autoIntervalMin'] = ha_to_petkit + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBLitterType(CoordinatorEntity, SelectEntity): + """Representation of litter box litter type.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_litter_type' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Litter type" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:grain' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if self.lb_data.device_detail['state']['pim'] != 0: + return True + else: + return False + + @property + def current_option(self) -> str: + """Return currently selected type.""" + + return LITTER_TYPE_NAMED[self.lb_data.device_detail['settings']['sandType']] + + @property + def options(self) -> list[str]: + """Return list of all available litter types.""" + + litter_types = list(LITTER_TYPE_NAMED.values()) + return litter_types + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + + ha_to_petkit = LITTER_TYPE_TO_PETKIT.get(option) + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.SANDTYPE, ha_to_petkit) + self.lb_data.device_detail['settings']['sandType'] = ha_to_petkit + self.async_write_ha_state() + await self.coordinator.async_request_refresh() From 9aa51732e687bfcab00c1d99c80414e9cbc01650 Mon Sep 17 00:00:00 2001 From: Robert Drinovac <52541649+RobertD502@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:51:28 -0500 Subject: [PATCH 08/11] add infinity feeder and pura x support --- custom_components/petkit/sensor.py | 1444 +++++++++++++++++++++++++++- 1 file changed, 1428 insertions(+), 16 deletions(-) diff --git a/custom_components/petkit/sensor.py b/custom_components/petkit/sensor.py index f6ae98b..2ef2916 100644 --- a/custom_components/petkit/sensor.py +++ b/custom_components/petkit/sensor.py @@ -4,7 +4,7 @@ from datetime import datetime from typing import Any -from petkitaio.model import Feeder, W5Fountain +from petkitaio.model import Feeder, LitterBox, Pet, W5Fountain from homeassistant.components.sensor import ( SensorDeviceClass, @@ -23,8 +23,18 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN, FEEDERS, WATER_FOUNTAINS +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM + +from .const import ( + DOMAIN, + EVENT_DESCRIPTION, + EVENT_TYPE_NAMED, + FEEDERS, + LITTER_BOXES, + LOGGER, + VALID_EVENT_TYPES, + WATER_FOUNTAINS +) from .coordinator import PetKitDataUpdateCoordinator @@ -43,6 +53,7 @@ async def async_setup_entry( WFEnergyUse(coordinator, wf_id), WFLastUpdate(coordinator, wf_id), WFFilter(coordinator, wf_id), + WFPurifiedWater(coordinator, wf_id), )) for feeder_id, feeder_data in coordinator.data.feeders.items(): @@ -54,14 +65,53 @@ async def async_setup_entry( FeederRSSI(coordinator, feeder_id), )) - # D4 Feeder - if feeder_data.type == 'd4': + # D3 & D4 + if feeder_data.type in ['d3', 'd4']: sensors.extend(( - TotalDispensed(coordinator, feeder_id), + TimesDispensed(coordinator, feeder_id), TotalPlanned(coordinator, feeder_id), PlannedDispensed(coordinator, feeder_id), + TotalDispensed(coordinator, feeder_id), + )) + + # D4 Feeder + if feeder_data.type == 'd4': + sensors.extend(( ManualDispensed(coordinator, feeder_id), - TimesDispensed(coordinator, feeder_id), + )) + + #D3 Feeder + if feeder_data.type == 'd3': + sensors.extend(( + AmountEaten(coordinator, feeder_id), + TimesEaten(coordinator, feeder_id), + FoodInBowl(coordinator, feeder_id), + FeederError(coordinator, feeder_id), + )) + + # Litter boxes + for lb_id, lb_data in coordinator.data.litter_boxes.items(): + # Pura X + sensors.extend(( + LBDeodorizerLevel(coordinator, lb_id), + LBLitterLevel(coordinator, lb_id), + LBLitterWeight(coordinator, lb_id), + LBRSSI(coordinator, lb_id), + LBError(coordinator, lb_id), + LBTimesUsed(coordinator, lb_id), + LBAverageUse(coordinator, lb_id), + LBTotalUse(coordinator, lb_id), + LBLastUsedBy(coordinator, lb_id), + LBLastEvent(coordinator, lb_id), + )) + + # Pets + for pet_id, pet_data in coordinator.data.pets.items(): + # Only add sensor for cats that have litter box(s) + if (pet_data.type == 'Cat') and coordinator.data.litter_boxes: + sensors.extend(( + PetRecentWeight(coordinator, pet_id), + PetLastUseDuration(coordinator, pet_id), )) async_add_entities(sensors) @@ -114,9 +164,8 @@ def has_entity_name(self) -> bool: def native_value(self) -> float: """Return total energy usage in kWh.""" - waterPumpRunTime = self.wf_data.data['waterPumpRunTime'] todayPumpRunTime = self.wf_data.data['todayPumpRunTime'] - energy_usage = round((waterPumpRunTime/todayPumpRunTime) * 0.002, 3) + energy_usage = round(((0.75 * todayPumpRunTime) / 3600000), 4) return energy_usage @property @@ -293,6 +342,76 @@ def icon(self) -> str: else: return 'mdi:filter' +class WFPurifiedWater(CoordinatorEntity, SensorEntity): + """Representation of amount of times water has been purified today""" + + def __init__(self, coordinator, wf_id): + super().__init__(coordinator) + self.wf_id = wf_id + + @property + def wf_data(self) -> W5Fountain: + """Handle coordinator Water Fountain data""" + + return self.coordinator.data.water_fountains[self.wf_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.wf_data.id)}, + "name": self.wf_data.data['name'], + "manufacturer": "PetKit", + "model": WATER_FOUNTAINS[self.wf_data.type], + "sw_version": f'{self.wf_data.data["hardware"]}.{self.wf_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.wf_data.id) + '_purified_water' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Purified water today" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def native_value(self) -> int: + """Return number of times water was purified today.""" + + f = ((1.5 * self.wf_data.data['todayPumpRunTime'])/60) + f2 = 2.0 + purified_today = int((f/f2)) + return purified_today + + @property + def icon(self) -> str | None: + """Set icon.""" + + return 'mdi:water-pump' + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.MEASUREMENT + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + class FeederStatus(CoordinatorEntity, SensorEntity): """Representation of feeder status.""" @@ -431,12 +550,6 @@ def state_class(self) -> SensorStateClass: return SensorStateClass.MEASUREMENT - @property - def entity_category(self) -> EntityCategory: - """Set category to diagnostic.""" - - return EntityCategory.DIAGNOSTIC - @property def icon(self) -> str | None: """Set icon.""" @@ -863,7 +976,10 @@ def has_entity_name(self) -> bool: def native_value(self) -> int: """Return total times dispensed.""" - return self.feeder_data.data['state']['feedState']['times'] + if self.feeder_data.type == 'd3': + return len(self.feeder_data.data['state']['feedState']['feedTimes']) + else: + return self.feeder_data.data['state']['feedState']['times'] @property def state_class(self) -> SensorStateClass: @@ -961,3 +1077,1299 @@ def icon(self) -> str: """Set icon.""" return 'mdi:wifi' + + +class AmountEaten(CoordinatorEntity, SensorEntity): + """Representation of amount eaten by pet today.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_amount_eaten' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Amount eaten" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def native_value(self) -> int: + """Return total amount eaten.""" + + return self.feeder_data.data['state']['feedState']['eatAmountTotal'] + + @property + def native_unit_of_measurement(self) -> UnitOfMass: + """Return grams as the native unit.""" + + return UnitOfMass.GRAMS + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.MEASUREMENT + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:food-drumstick' + + +class TimesEaten(CoordinatorEntity, SensorEntity): + """Representation of amount of times pet ate today.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_times_eaten' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Times eaten" + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def native_value(self) -> int: + """Return total times eaten.""" + + return len(self.feeder_data.data['state']['feedState']['eatTimes']) + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.MEASUREMENT + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:food-drumstick' + + +class FoodInBowl(CoordinatorEntity, SensorEntity): + """Representation of amount of food in D3 feeder bowl.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_food_in_bowl' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Food in bowl" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def native_value(self) -> int: + """Return current amount of food in bowl.""" + + return self.feeder_data.data['state']['weight'] + + @property + def native_unit_of_measurement(self) -> UnitOfMass: + """Return grams as the native unit.""" + + return UnitOfMass.GRAMS + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.MEASUREMENT + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:food-drumstick' + + +class FeederError(CoordinatorEntity, SensorEntity): + """Representation of D3 feeder error.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_feeder_error' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Error" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def native_value(self) -> str: + """Return current error if there is one.""" + + if 'errorMsg' in self.feeder_data.data['state']: + return self.feeder_data.data['state']['errorMsg'] + else: + return 'No Error' + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:alert-circle' + + +class LBDeodorizerLevel(CoordinatorEntity, SensorEntity): + """Representation of litter box deodorizer percentage left.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_deodorizer_level' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Deodorizer level" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:spray-bottle' + + @property + def native_value(self) -> int: + """Return current percentage.""" + + return self.lb_data.device_detail['state']['liquid'] + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.MEASUREMENT + + @property + def native_unit_of_measurement(self) -> str: + """Return percent as the native unit.""" + + return PERCENTAGE + +class LBLitterLevel(CoordinatorEntity, SensorEntity): + """Representation of litter box litter percentage left.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_litter_level' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Litter level" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:landslide' + + @property + def native_value(self) -> int: + """Return current percentage.""" + + return self.lb_data.device_detail['state']['sandPercent'] + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.MEASUREMENT + + @property + def native_unit_of_measurement(self) -> str: + """Return percent as the native unit.""" + + return PERCENTAGE + + +class LBLitterWeight(CoordinatorEntity, SensorEntity): + """Representation of litter box litter weight.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_litter_weight' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Litter weight" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:landslide' + + @property + def native_value(self) -> float: + """Return current weight in Kg.""" + + return round((self.lb_data.device_detail['state']['sandWeight'] / 1000), 1) + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.TOTAL + + @property + def native_unit_of_measurement(self) -> UnitOfMass: + """Return Kg as the native unit.""" + + return UnitOfMass.KILOGRAMS + + @property + def device_class(self) -> SensorDeviceClass: + """Return entity device class.""" + + return SensorDeviceClass.WEIGHT + + +class LBRSSI(CoordinatorEntity, SensorEntity): + """Representation of litter box wifi strength.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_rssi' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "RSSI" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:wifi' + + @property + def native_value(self) -> int: + """Return current signal strength.""" + + return self.lb_data.device_detail['state']['wifi']['rsq'] + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.MEASUREMENT + + @property + def native_unit_of_measurement(self) -> str: + """Return dBm as the native unit.""" + + return SIGNAL_STRENGTH_DECIBELS_MILLIWATT + + @property + def device_class(self) -> SensorDeviceClass: + """Return entity device class.""" + + return SensorDeviceClass.SIGNAL_STRENGTH + + +class LBError(CoordinatorEntity, SensorEntity): + """Representation of litter box error.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_error' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Error" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def native_value(self) -> str: + """Return current error if there is one.""" + + if 'errorMsg' in self.lb_data.device_detail['state']: + return self.lb_data.device_detail['state']['errorMsg'] + else: + return 'No Error' + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:alert-circle' + + +class LBTimesUsed(CoordinatorEntity, SensorEntity): + """Representation of litter box usage count.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_times_used' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Times used" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def native_value(self) -> int: + """Return current usage count.""" + + return self.lb_data.statistics['times'] + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.TOTAL + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:counter' + + +class LBAverageUse(CoordinatorEntity, SensorEntity): + """Representation of litter box average usage.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_average_use' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Average use" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def native_value(self) -> int: + """Return current usage time average in seconds.""" + + return self.lb_data.statistics['avgTime'] + + @property + def native_unit_of_measurement(self) -> UnitOfTime: + """Return seconds as the native unit.""" + + return UnitOfTime.SECONDS + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.MEASUREMENT + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:clock' + + +class LBTotalUse(CoordinatorEntity, SensorEntity): + """Representation of litter box total usage.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_total_use' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Total use" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def native_value(self) -> int: + """Return current usage time average in seconds.""" + + return self.lb_data.statistics['totalTime'] + + @property + def native_unit_of_measurement(self) -> UnitOfTime: + """Return seconds as the native unit.""" + + return UnitOfTime.SECONDS + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.MEASUREMENT + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:clock' + + +class LBLastUsedBy(CoordinatorEntity, SensorEntity): + """Representation of last pet to use the litter box.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_last_used_by' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Last used by" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def native_value(self) -> str: + """Return last pet to use the litter box.""" + + if self.lb_data.statistics['statisticInfo']: + last_record = self.lb_data.statistics['statisticInfo'][-1] + if last_record['petId'] == '0': + return 'Unknown pet' + else: + return last_record['petName'] + else: + return 'No record yet' + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:cat' + + +class LBLastEvent(CoordinatorEntity, SensorEntity): + """Representation of last litter box event.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + self.sub_events = None + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_last_event' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Last event" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def native_value(self) -> str: + """Return last litter box event from device record.""" + + if self.lb_data.device_record: + last_record = self.lb_data.device_record[-1] + if last_record['subContent']: + self.sub_events = self.sub_events_to_description(last_record['subContent']) + else: + self.sub_events = None + event = self.result_to_description(last_record['eventType'], last_record) + return event + else: + return 'No events yet' + + @property + def extra_state_attributes(self): + """Return sub events associated with the main event.""" + + return { + 'sub_events': self.sub_events + } + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:calendar' + + @property + def entity_category(self) -> EntityCategory: + """Set category to diagnostic.""" + + return EntityCategory.DIAGNOSTIC + + def result_to_description(self, event_type: int, record: dict[str, Any]) -> str: + """Return a description of the last event""" + + # Make sure event_type is valid + if event_type not in VALID_EVENT_TYPES: + return 'Event type unknown' + + # Pet out events don't have result or reason + if event_type != 10: + result = record['content']['result'] + if 'startReason' in record['content']: + reason = record['content']['startReason'] + + if event_type == 5: + if result == 2: + if 'error' in record['content']: + error = record['content']['error'] + else: + return EVENT_TYPE_NAMED[event_type] + + try: + description = EVENT_DESCRIPTION[event_type][result][reason][error] + except KeyError: + return EVENT_TYPE_NAMED[event_type] + return description + + else: + try: + description = EVENT_DESCRIPTION[event_type][result][reason] + except KeyError: + return EVENT_TYPE_NAMED[event_type] + return description + + if event_type in [6, 7]: + if result == 2: + if 'error' in record['content']: + error = record['content']['error'] + else: + return EVENT_TYPE_NAMED[event_type] + + try: + description = EVENT_DESCRIPTION[event_type][result][error] + except KeyError: + return EVENT_TYPE_NAMED[event_type] + return description + + else: + try: + description = EVENT_DESCRIPTION[event_type][result] + except KeyError: + return EVENT_TYPE_NAMED[event_type] + return description + + if event_type == 8: + try: + description = EVENT_DESCRIPTION[event_type][result][reason] + except KeyError: + return EVENT_TYPE_NAMED[event_type] + return description + + if event_type == 10: + if (record['petId'] == '-2') or (record['petId'] == '-1'): + name = 'Unknown' + else: + name = record['petName'] + return f'{name} used the litter box' + + def sub_events_to_description(self, sub_events: list[dict[str, Any]]) -> list[str]: + """Create a list containing all of the sub events associated with an event to be used as attribute""" + + event_list: list[str] = [] + for event in sub_events: + description = self.result_to_description(event['eventType'], event) + event_list.append(description) + return event_list + +class PetRecentWeight(CoordinatorEntity, SensorEntity): + """Representation of most recent weight measured by litter box.""" + + def __init__(self, coordinator, pet_id): + super().__init__(coordinator) + self.pet_id = pet_id + + @property + def pet_data(self) -> Pet: + """Handle coordinator Pet data.""" + + return self.coordinator.data.pets[self.pet_id] + + @property + def litter_boxes(self) -> dict[LitterBox, Any]: + """Handle coordinator Litter Boxes data.""" + + return self.coordinator.data.litter_boxes + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.pet_data.id)}, + "name": self.pet_data.data['name'], + "manufacturer": "PetKit", + "model": self.pet_data.type, + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return self.pet_data.id + '_recent_weight' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Latest weight" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def entity_picture(self) -> str: + """Grab associated pet picture.""" + + if 'avatar' in self.pet_data.data: + return self.pet_data.data['avatar'] + else: + return None + + @property + def icon(self) -> str: + """Set icon if the pet doesn't have an avatar.""" + + if 'avatar' in self.pet_data.data: + return None + else: + return 'mdi:weight' + + @property + def native_value(self) -> float: + """Return most recent weight from today.""" + + sorted_dict = self.grab_recent_weight() + if sorted_dict: + last_key = list(sorted_dict)[-1] + latest_weight = sorted_dict[last_key] + weight_calculation = round((latest_weight / 1000), 1) + return weight_calculation + else: + return 0.0 + + @property + def native_unit_of_measurement(self) -> UnitOfMass: + """Return kilograms as the native unit.""" + + return UnitOfMass.KILOGRAMS + + @property + def device_class(self) -> SensorDeviceClass: + """Return entity device class.""" + + return SensorDeviceClass.WEIGHT + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.TOTAL + + def grab_recent_weight(self) -> float: + """Grab the most recent weight.""" + + weight_dict: dict[int, int] = {} + + for lb_id, lb_data in self.litter_boxes.items(): + if lb_data.statistics['statisticInfo']: + try: + final_idx = max(index for index, stat in enumerate(lb_data.statistics['statisticInfo']) if stat['petId'] == self.pet_data.id) + except ValueError: + continue + else: + last_stat = lb_data.statistics['statisticInfo'][final_idx] + weight = last_stat['petWeight'] + time = last_stat['xTime'] + weight_dict[time] = weight + sorted_dict = dict(sorted(weight_dict.items())) + return sorted_dict + + +class PetLastUseDuration(CoordinatorEntity, SensorEntity): + """Representation of most recent litter box use duration.""" + + def __init__(self, coordinator, pet_id): + super().__init__(coordinator) + self.pet_id = pet_id + + @property + def pet_data(self) -> Pet: + """Handle coordinator Pet data.""" + + return self.coordinator.data.pets[self.pet_id] + + @property + def litter_boxes(self) -> dict[LitterBox, Any]: + """Handle coordinator Litter Boxes data.""" + + return self.coordinator.data.litter_boxes + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.pet_data.id)}, + "name": self.pet_data.data['name'], + "manufacturer": "PetKit", + "model": self.pet_data.type, + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return self.pet_data.id + '_last_use_duration' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Last use duration" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def entity_picture(self) -> str: + """Grab associated pet picture.""" + + if 'avatar' in self.pet_data.data: + return self.pet_data.data['avatar'] + else: + return None + + @property + def icon(self) -> str: + """Set icon if the pet doesn't have an avatar.""" + + if 'avatar' in self.pet_data.data: + return None + else: + return 'mdi:clock' + + @property + def native_value(self) -> int: + """Return most recent duration from today.""" + + sorted_dict = self.grab_recent_duration() + if sorted_dict: + last_key = list(sorted_dict)[-1] + latest_duration = sorted_dict[last_key] + return latest_duration + else: + return 0 + + @property + def native_unit_of_measurement(self) -> UnitOfTime: + """Return seconds as the native unit.""" + + return UnitOfTime.SECONDS + + @property + def state_class(self) -> SensorStateClass: + """Return the type of state class.""" + + return SensorStateClass.MEASUREMENT + + def grab_recent_duration(self) -> float: + """Grab the most recent duration.""" + + duration_dict: dict[int, int] = {} + + for lb_id, lb_data in self.litter_boxes.items(): + if lb_data.statistics['statisticInfo']: + try: + final_idx = max(index for index, stat in enumerate(lb_data.statistics['statisticInfo']) if stat['petId'] == self.pet_data.id) + # Handle if the pet didn't use the litter box + except ValueError: + continue + else: + last_stat = lb_data.statistics['statisticInfo'][final_idx] + duration = last_stat['petTotalTime'] + time = last_stat['xTime'] + duration_dict[time] = duration + sorted_dict = dict(sorted(duration_dict.items())) + return sorted_dict From 94416242429d7ef487cf3af5f4b87a5861fe8e46 Mon Sep 17 00:00:00 2001 From: Robert Drinovac <52541649+RobertD502@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:52:42 -0500 Subject: [PATCH 09/11] add infinity feeder and pura x support --- custom_components/petkit/switch.py | 1462 +++++++++++++++++++++++++++- 1 file changed, 1438 insertions(+), 24 deletions(-) diff --git a/custom_components/petkit/switch.py b/custom_components/petkit/switch.py index 1cc9a0b..289b809 100644 --- a/custom_components/petkit/switch.py +++ b/custom_components/petkit/switch.py @@ -4,17 +4,18 @@ from typing import Any import asyncio -from petkitaio.constants import FeederSetting, W5Command +from petkitaio.constants import FeederSetting, LitterBoxCommand, LitterBoxSetting, W5Command from petkitaio.exceptions import BluetoothError -from petkitaio.model import Feeder, W5Fountain +from petkitaio.model import Feeder, LitterBox, W5Fountain from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, FEEDERS, WATER_FOUNTAINS +from .const import DOMAIN, FEEDERS, LITTER_BOXES, WATER_FOUNTAINS from .coordinator import PetKitDataUpdateCoordinator from .exceptions import PetKitBluetoothError @@ -51,6 +52,32 @@ async def async_setup_entry( DispenseTone(coordinator, feeder_id), )) + # D3 Feeder + if feeder_data.type == 'd3': + switches.extend(( + VoiceDispense(coordinator, feeder_id), + DoNotDisturb(coordinator, feeder_id), + SurplusControl(coordinator, feeder_id), + SystemNotification(coordinator, feeder_id), + )) + + # Litter boxes + for lb_id, lb_data in coordinator.data.litter_boxes.items(): + # Pura X + switches.extend(( + LBAutoOdor(coordinator, lb_id), + LBAutoClean(coordinator, lb_id), + LBAvoidRepeat(coordinator, lb_id), + LBDoNotDisturb(coordinator, lb_id), + LBPeriodicCleaning(coordinator, lb_id), + LBPeriodicOdor(coordinator, lb_id), + LBKittenMode(coordinator, lb_id), + LBDisplay(coordinator, lb_id), + LBChildLock(coordinator, lb_id), + LBLightWeight(coordinator, lb_id), + LBPower(coordinator, lb_id), + )) + async_add_entities(switches) @@ -101,9 +128,7 @@ def has_entity_name(self) -> bool: def icon(self) -> str: """Set icon.""" - is_on = self.wf_data.data['settings']['lampRingSwitch'] == 1 - - if is_on: + if self.is_on: return 'mdi:lightbulb' else: return 'mdi:lightbulb-off' @@ -200,9 +225,7 @@ def has_entity_name(self) -> bool: def icon(self) -> str: """Set icon.""" - is_on = self.wf_data.data['powerStatus'] == 1 - - if is_on: + if self.is_on: return 'mdi:power-plug' else: return 'mdi:power-plug-off' @@ -312,13 +335,17 @@ def has_entity_name(self) -> bool: def icon(self) -> str: """Set icon.""" - is_on = self.wf_data.data['settings']['noDisturbingSwitch'] == 1 - - if is_on: + if self.is_on: return 'mdi:sleep' else: return 'mdi:sleep-off' + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + @property def is_on(self) -> bool: """Determine if DND is on.""" @@ -411,9 +438,7 @@ def has_entity_name(self) -> bool: def icon(self) -> str: """Set icon.""" - is_on = self.feeder_data.data['settings']['lightMode'] == 1 - - if is_on: + if self.is_on: return 'mdi:lightbulb' else: return 'mdi:lightbulb-off' @@ -504,13 +529,17 @@ def has_entity_name(self) -> bool: def icon(self) -> str: """Set icon.""" - is_on = self.feeder_data.data['settings']['manualLock'] == 1 - - if is_on: + if self.is_on: return 'mdi:lock' else: return 'mdi:lock-open' + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + @property def is_on(self) -> bool: """Determine if child lock is on.""" @@ -597,13 +626,17 @@ def has_entity_name(self) -> bool: def icon(self) -> str: """Set icon.""" - is_on = self.feeder_data.data['settings']['foodWarn'] == 1 - - if is_on: + if self.is_on: return 'mdi:alarm' else: return 'mdi:alarm-off' + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + @property def is_on(self) -> bool: """Determine if food shortage alarm is on.""" @@ -682,13 +715,17 @@ def has_entity_name(self) -> bool: def icon(self) -> str: """Set icon.""" - is_on = self.feeder_data.data['settings']['feedSound'] == 1 - - if is_on: + if self.is_on: return 'mdi:ear-hearing' else: return 'mdi:ear-hearing-off' + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + @property def is_on(self) -> bool: """Determine if food shortage alarm is on.""" @@ -719,3 +756,1380 @@ async def async_turn_off(self, **kwargs) -> None: self.feeder_data.data['settings']['feedSound'] = 0 self.async_write_ha_state() await self.coordinator.async_request_refresh() + + +class VoiceDispense(CoordinatorEntity, SwitchEntity): + """Representation of D3 Feeder Voice with dispense.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_voice_dispense' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Voice with dispense" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.is_on: + return 'mdi:account-voice' + else: + return 'mdi:account-voice-off' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if voice with dispense is on.""" + + return self.feeder_data.data['settings']['soundEnable'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if self.feeder_data.data['state']['pim'] != 0: + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn voice with dispense on.""" + + await self.coordinator.client.update_feeder_settings(self.feeder_data, FeederSetting.SOUNDENABLE, 1) + + self.feeder_data.data['settings']['soundEnable'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn voice with dispense off.""" + + await self.coordinator.client.update_feeder_settings(self.feeder_data, FeederSetting.SOUNDENABLE, 0) + + self.feeder_data.data['settings']['soundEnable'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class DoNotDisturb(CoordinatorEntity, SwitchEntity): + """Representation of D3 Feeder DND.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_do_not_disturb' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Do not disturb" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.is_on: + return 'mdi:sleep' + else: + return 'mdi:sleep-off' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if DND is on.""" + + return self.feeder_data.data['settings']['disturbMode'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if self.feeder_data.data['state']['pim'] != 0: + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn DND on.""" + + await self.coordinator.client.update_feeder_settings(self.feeder_data, FeederSetting.DONOTDISTURB, 1) + + self.feeder_data.data['settings']['disturbMode'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn DND off.""" + + await self.coordinator.client.update_feeder_settings(self.feeder_data, FeederSetting.DONOTDISTURB, 0) + + self.feeder_data.data['settings']['disturbMode'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class SurplusControl(CoordinatorEntity, SwitchEntity): + """Representation of D3 Feeder Surplus Control.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_surplus_control' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Surplus control" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.is_on: + return 'mdi:food-drumstick' + else: + return 'mdi:food-drumstick-off' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if surplus control is on.""" + + return self.feeder_data.data['settings']['surplusControl'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if self.feeder_data.data['state']['pim'] != 0: + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn surplus control on.""" + + await self.coordinator.client.update_feeder_settings(self.feeder_data, FeederSetting.SURPLUSCONTROL, 1) + + self.feeder_data.data['settings']['surplusControl'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn surplus control off.""" + + await self.coordinator.client.update_feeder_settings(self.feeder_data, FeederSetting.SURPLUSCONTROL, 0) + + self.feeder_data.data['settings']['surplusControl'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class SystemNotification(CoordinatorEntity, SwitchEntity): + """Representation of D3 Feeder System Notification sound.""" + + def __init__(self, coordinator, feeder_id): + super().__init__(coordinator) + self.feeder_id = feeder_id + + @property + def feeder_data(self) -> Feeder: + """Handle coordinator Feeder data.""" + + return self.coordinator.data.feeders[self.feeder_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.feeder_data.id)}, + "name": self.feeder_data.data['name'], + "manufacturer": "PetKit", + "model": FEEDERS[self.feeder_data.type], + "sw_version": f'{self.feeder_data.data["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.feeder_data.id) + '_system_notification' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "System notification sound" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.is_on: + return 'mdi:bell-ring' + else: + return 'mdi:bell-off' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if system notification is on.""" + + return self.feeder_data.data['settings']['systemSoundEnable'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if self.feeder_data.data['state']['pim'] != 0: + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn system notification on.""" + + await self.coordinator.client.update_feeder_settings(self.feeder_data, FeederSetting.SYSTEMSOUND, 1) + + self.feeder_data.data['settings']['systemSoundEnable'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn system notification off.""" + + await self.coordinator.client.update_feeder_settings(self.feeder_data, FeederSetting.SYSTEMSOUND, 0) + + self.feeder_data.data['settings']['systemSoundEnable'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBAutoOdor(CoordinatorEntity, SwitchEntity): + """Representation of litter box auto odor removal.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_auto_odor' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Auto odor removal" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.is_on: + return 'mdi:scent' + else: + return 'mdi:scent-off' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if auto odor removal is on.""" + + return self.lb_data.device_detail['settings']['autoRefresh'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if self.lb_data.device_detail['state']['pim'] != 0: + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn auto odor removal on.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.AUTOODOR, 1) + + self.lb_data.device_detail['settings']['autoRefresh'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn auto odor removal off.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.AUTOODOR, 0) + + self.lb_data.device_detail['settings']['autoRefresh'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBAutoClean(CoordinatorEntity, SwitchEntity): + """Representation of litter box auto cleaning switch.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_auto_clean' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Auto cleaning" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:vacuum' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if auto cleaning is on.""" + + return self.lb_data.device_detail['settings']['autoWork'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if (self.lb_data.device_detail['state']['pim'] != 0) and (self.lb_data.device_detail['settings']['kitten'] != 1): + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn auto cleaning on.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.AUTOCLEAN, 1) + + self.lb_data.device_detail['settings']['autoWork'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn auto cleaning off.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.AUTOCLEAN, 0) + + self.lb_data.device_detail['settings']['autoWork'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBAvoidRepeat(CoordinatorEntity, SwitchEntity): + """Representation of litter box avoid repeat cleaning switch.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_avoid_repeat' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Avoid repeat cleaning" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.is_on: + return 'mdi:repeat' + else: + return 'mdi:repeat-off' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if avoid repeat is on.""" + + return self.lb_data.device_detail['settings']['avoidRepeat'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if (self.lb_data.device_detail['state']['pim'] != 0) and (self.lb_data.device_detail['settings']['kitten'] != 1): + # Only available if automatic cleaning is turned on + if self.lb_data.device_detail['settings']['autoWork'] != 0: + return True + else: + return False + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn avoid repeat cleaning on.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.AVOIDREPEATCLEAN, 1) + + self.lb_data.device_detail['settings']['avoidRepeat'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn avoid repeat cleaning off.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.AVOIDREPEATCLEAN, 0) + + self.lb_data.device_detail['settings']['avoidRepeat'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBDoNotDisturb(CoordinatorEntity, SwitchEntity): + """Representation of litter box dnd switch.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_dnd' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Do not disturb" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.is_on: + return 'mdi:sleep' + else: + return 'mdi:sleep-off' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if dnd is on.""" + + return self.lb_data.device_detail['settings']['disturbMode'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if (self.lb_data.device_detail['state']['pim'] != 0): + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn dnd on.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.DONOTDISTURB, 1) + + self.lb_data.device_detail['settings']['disturbMode'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn dnd off.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.DONOTDISTURB, 0) + + self.lb_data.device_detail['settings']['disturbMode'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBPeriodicCleaning(CoordinatorEntity, SwitchEntity): + """Representation of litter box periodic cleaning switch.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_periodic_cleaning' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Periodic cleaning" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.is_on: + return 'mdi:timer' + else: + return 'mdi:timer-off' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if periodic cleaning is on.""" + + return self.lb_data.device_detail['settings']['fixedTimeClear'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if (self.lb_data.device_detail['state']['pim'] != 0) and (self.lb_data.device_detail['settings']['kitten'] != 1): + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn periodic cleaning on.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.PERIODICCLEAN, 1) + + self.lb_data.device_detail['settings']['fixedTimeClear'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn periodic cleaning off.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.PERIODICCLEAN, 0) + + self.lb_data.device_detail['settings']['fixedTimeClear'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBPeriodicOdor(CoordinatorEntity, SwitchEntity): + """Representation of litter box periodic odor removal.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_periodic_odor' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Periodic odor removal" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.is_on: + return 'mdi:scent' + else: + return 'mdi:scent-off' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if periodic odor removal is on.""" + + return self.lb_data.device_detail['settings']['fixedTimeRefresh'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if (self.lb_data.device_detail['state']['pim'] != 0): + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn periodic odor removal on.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.PERIODICODOR, 1) + + self.lb_data.device_detail['settings']['fixedTimeRefresh'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn periodic odor removal off.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.PERIODICODOR, 0) + + self.lb_data.device_detail['settings']['fixedTimeRefresh'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBKittenMode(CoordinatorEntity, SwitchEntity): + """Representation of litter box kitten mode.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_kitten_mode' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Kitten mode" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:cat' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if kitten mode is on.""" + + return self.lb_data.device_detail['settings']['kitten'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if (self.lb_data.device_detail['state']['pim'] != 0): + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn kitten mode on.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.KITTENMODE, 1) + + self.lb_data.device_detail['settings']['kitten'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn kitten mode off.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.KITTENMODE, 0) + + self.lb_data.device_detail['settings']['kitten'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBDisplay(CoordinatorEntity, SwitchEntity): + """Representation of litter box display power.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_display' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Display" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.is_on: + return 'mdi:monitor' + else: + return 'mdi:monitor-off' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if display is on.""" + + return self.lb_data.device_detail['settings']['lightMode'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if (self.lb_data.device_detail['state']['pim'] != 0): + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn display on.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.DISPLAY, 1) + + self.lb_data.device_detail['settings']['lightMode'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn display off.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.DISPLAY, 0) + + self.lb_data.device_detail['settings']['lightMode'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBChildLock(CoordinatorEntity, SwitchEntity): + """Representation of litter box child lock.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_child_lock' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Child lock" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.is_on: + return 'mdi:lock' + else: + return 'mdi:lock-off' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if child lock is on.""" + + return self.lb_data.device_detail['settings']['manualLock'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if (self.lb_data.device_detail['state']['pim'] != 0): + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn child lock on.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.CHILDLOCK, 1) + + self.lb_data.device_detail['settings']['manualLock'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn child lock off.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.CHILDLOCK, 0) + + self.lb_data.device_detail['settings']['manualLock'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBLightWeight(CoordinatorEntity, SwitchEntity): + """Representation of litter box light weight cleaning disabler.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_light_weight' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Light weight cleaning disabled" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + return 'mdi:feather' + + @property + def entity_category(self) -> EntityCategory: + """Set category to config.""" + + return EntityCategory.CONFIG + + @property + def is_on(self) -> bool: + """Determine if light weight disabler is on.""" + + return self.lb_data.device_detail['settings']['underweight'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + kitten_mode_off = self.lb_data.device_detail['settings']['kitten'] == 0 + auto_clean = self.lb_data.device_detail['settings']['autoWork'] == 1 + avoid_repeat = self.lb_data.device_detail['settings']['avoidRepeat'] == 1 + + if (self.lb_data.device_detail['state']['pim'] != 0): + # Kitten mode must be off and auto cleaning and avoid repeat must be on + if (kitten_mode_off and auto_clean and avoid_repeat): + return True + else: + return False + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn light weight disabler on.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.DISABLELIGHTWEIGHT, 1) + + self.lb_data.device_detail['settings']['underweight'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn light weight disabler off.""" + + await self.coordinator.client.update_litter_box_settings(self.lb_data, LitterBoxSetting.DISABLELIGHTWEIGHT, 0) + + self.lb_data.device_detail['settings']['underweight'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + +class LBPower(CoordinatorEntity, SwitchEntity): + """Representation of litter box power switch.""" + + def __init__(self, coordinator, lb_id): + super().__init__(coordinator) + self.lb_id = lb_id + + @property + def lb_data(self) -> LitterBox: + """Handle coordinator litter box data.""" + + return self.coordinator.data.litter_boxes[self.lb_id] + + @property + def device_info(self) -> dict[str, Any]: + """Return device registry information for this entity.""" + + return { + "identifiers": {(DOMAIN, self.lb_data.id)}, + "name": self.lb_data.device_detail['name'], + "manufacturer": "PetKit", + "model": LITTER_BOXES[self.lb_data.type], + "sw_version": f'{self.lb_data.device_detail["firmware"]}' + } + + @property + def unique_id(self) -> str: + """Sets unique ID for this entity.""" + + return str(self.lb_data.id) + '_power' + + @property + def name(self) -> str: + """Return name of the entity.""" + + return "Power" + + @property + def has_entity_name(self) -> bool: + """Indicate that entity has name defined.""" + + return True + + @property + def icon(self) -> str: + """Set icon.""" + + if self.is_on: + return 'mdi:power' + else: + return 'mdi:power-off' + + @property + def is_on(self) -> bool: + """Determine if litter box is powered on.""" + + return self.lb_data.device_detail['state']['power'] == 1 + + @property + def available(self) -> bool: + """Only make available if device is online.""" + + if (self.lb_data.device_detail['state']['pim'] != 0): + return True + else: + return False + + async def async_turn_on(self, **kwargs) -> None: + """Turn power on.""" + + await self.coordinator.client.control_litter_box(self.lb_data, LitterBoxCommand.POWER) + + self.lb_data.device_detail['state']['power'] = 1 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs) -> None: + """Turn power off.""" + + await self.coordinator.client.control_litter_box(self.lb_data, LitterBoxCommand.POWER) + + self.lb_data.device_detail['state']['power'] = 0 + self.async_write_ha_state() + await self.coordinator.async_request_refresh() From da01e3e1d4d7fe91c3730f8a808e71ebd8e1941e Mon Sep 17 00:00:00 2001 From: Robert Drinovac <52541649+RobertD502@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:53:37 -0500 Subject: [PATCH 10/11] remove LOGGER import --- custom_components/petkit/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/petkit/sensor.py b/custom_components/petkit/sensor.py index 2ef2916..922b32f 100644 --- a/custom_components/petkit/sensor.py +++ b/custom_components/petkit/sensor.py @@ -31,7 +31,6 @@ EVENT_TYPE_NAMED, FEEDERS, LITTER_BOXES, - LOGGER, VALID_EVENT_TYPES, WATER_FOUNTAINS ) From 15210dabd47c7d02b9b5fd7900e9072809049ccc Mon Sep 17 00:00:00 2001 From: Robert Drinovac <52541649+RobertD502@users.noreply.github.com> Date: Wed, 22 Feb 2023 19:12:01 -0500 Subject: [PATCH 11/11] Measurement state class for pet recent weight and litter box litter weight --- custom_components/petkit/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/petkit/sensor.py b/custom_components/petkit/sensor.py index 922b32f..830e434 100644 --- a/custom_components/petkit/sensor.py +++ b/custom_components/petkit/sensor.py @@ -1570,7 +1570,7 @@ def entity_category(self) -> EntityCategory: def state_class(self) -> SensorStateClass: """Return the type of state class.""" - return SensorStateClass.TOTAL + return SensorStateClass.MEASUREMENT @property def native_unit_of_measurement(self) -> UnitOfMass: @@ -2241,7 +2241,7 @@ def device_class(self) -> SensorDeviceClass: def state_class(self) -> SensorStateClass: """Return the type of state class.""" - return SensorStateClass.TOTAL + return SensorStateClass.MEASUREMENT def grab_recent_weight(self) -> float: """Grab the most recent weight."""