Skip to content

Commit

Permalink
Merge pull request #20 from RobertD502/fresh_element_support
Browse files Browse the repository at this point in the history
Add Fresh Element feeder support
  • Loading branch information
RobertD502 authored Jul 6, 2023
2 parents e67b0af + 07f8611 commit eb14d8e
Show file tree
Hide file tree
Showing 11 changed files with 407 additions and 5 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Custom Home Assistant component for controlling and monitoring PetKit devices an
- [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/)
- [Fresh Element Gemini](https://www.amazon.com/PETKIT-Automatic-Combination-Dispenser-Stainless/dp/B0BF56RTQH)
- [Fresh Element](https://petkit.us/products/petkit-element-wi-fi-enabled-smart-pet-food-container-feeder)

`Litter Boxes`
- [Pura X Litter Box](https://www.amazon.com/PETKIT-Self-Cleaning-Scooping-Automatic-Multiple/dp/B08T9CCP1M)
Expand Down Expand Up @@ -238,6 +239,36 @@ Each Feeder has the following entities:

</details>


<details>
<summary> <b>Fresh Element</b> (<i>click to expand</i>)</summary>
<!---->
<br/>
Each Feeder has the following entities:
<br/>

| Entity | Entity Type | Additional Comments |
|----------------------------|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `Cancel manual feed` | `Button` | - Only available if your feeder is online (connected to PetKit's servers). <br/>- 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` | - Allows setting the amount of food (grams) to dispense immediately. <br/>- 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. <br/>- Only available if your feeder is online (connected to PetKit's servers). |
| `Start calibration` | `Button` | See note at the bottom of the table for description provided by PetKit. |
| `Stop calibration` | `Button` | |
| `Battery status` | `Sensor` | - Will only become available when feeder is running on batteries. <br/>- Indicates the battery level (Normal or Low). |
| `Error` | `Sensor` | Identifies any errors reported by the feeder. |
| `Food Left` | `Sensor` | Identifies the amount of food, as a percentage, left in the feeder. |
| `RSSI` | `Sensor` | WiFi connection strength. |
| `Status` | `Sensor` | `Normal` = Feeder is connected to PetKit's servers <br/>`Offline` = Feeder is not connected to PetKit servers <br/>`On Batteries` = If installed, feeder is currently being powered by the batteries. |

> Feeder Calibration: Calibration is required after every installation and removal of batteries from the feeder. Prior to calibration, be sure to clean out all of the food from the feeder as the calibration process will empty any food that is left in the feeder.
</details>


## Water Fountains
___

Expand Down
1 change: 1 addition & 0 deletions custom_components/petkit/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ def is_on(self) -> bool:
return False

if self.feeder_data.type != 'd3':
# The food key for the Fresh Element represents grams left
if self.feeder_data.data['state']['food'] == 0:
return True
else:
Expand Down
141 changes: 139 additions & 2 deletions custom_components/petkit/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Any
import asyncio

from petkitaio.constants import LitterBoxCommand, W5Command
from petkitaio.constants import FeederCommand, LitterBoxCommand, W5Command
from petkitaio.exceptions import BluetoothError
from petkitaio.model import Feeder, LitterBox, W5Fountain

Expand Down Expand Up @@ -43,7 +43,7 @@ async def async_setup_entry(
)

# D3, D4, and D4s
if feeder_data.type in ['d3', 'd4', 'd4s']:
if feeder_data.type in ['d3', 'd4', 'd4s', 'feeder']:
buttons.append(
CancelManualFeed(coordinator, feeder_id)
)
Expand All @@ -60,6 +60,13 @@ async def async_setup_entry(
FoodReplenished(coordinator, feeder_id)
)

# Fresh Element
if feeder_data.type == 'feeder':
buttons.extend((
StartFeederCal(coordinator, feeder_id),
StopFeederCal(coordinator, feeder_id)
))

# Litter boxes
for lb_id, lb_data in coordinator.data.litter_boxes.items():
# Pura X & Pura MAX
Expand Down Expand Up @@ -1369,3 +1376,133 @@ async def async_press(self) -> None:
self.feeder_data.data['state']['food2'] = 1
self.async_write_ha_state()
await self.coordinator.async_request_refresh()


class StartFeederCal(CoordinatorEntity, ButtonEntity):
"""Representation of fresh element feeder start calibration button."""

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) + '_start_cal'

@property
def has_entity_name(self) -> bool:
"""Indicate that entity has name defined."""

return True

@property
def translation_key(self) -> str:
"""Translation key for this entity."""

return "start_cal"

@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 entity_category(self) -> EntityCategory:
"""Set category to config."""

return EntityCategory.CONFIG

async def async_press(self) -> None:
"""Handle the button press."""

await self.coordinator.client.fresh_element_calibration(self.feeder_data, FeederCommand.START_CALIBRATION)
await self.coordinator.async_request_refresh()


class StopFeederCal(CoordinatorEntity, ButtonEntity):
"""Representation of fresh element feeder stop calibration button."""

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) + '_stop_cal'

@property
def has_entity_name(self) -> bool:
"""Indicate that entity has name defined."""

return True

@property
def translation_key(self) -> str:
"""Translation key for this entity."""

return "stop_cal"

@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 entity_category(self) -> EntityCategory:
"""Set category to config."""

return EntityCategory.CONFIG

async def async_press(self) -> None:
"""Handle the button press."""

await self.coordinator.client.fresh_element_calibration(self.feeder_data, FeederCommand.STOP_CALIBRATION)
await self.coordinator.async_request_refresh()
4 changes: 3 additions & 1 deletion custom_components/petkit/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
'd3': 'Fresh Element Infinity',
'd4': 'Fresh Element Solo',
'd4s': 'Fresh Element Gemini',
'feeder': 'Fresh Element',
'feedermini': 'Fresh Element Mini Pro',
}

Expand Down Expand Up @@ -134,6 +135,7 @@
'1/2 Cup (50g)'
]


LITTER_TYPE_NAMED = {
1: 'bentonite',
2: 'tofu',
Expand All @@ -151,5 +153,5 @@
35: '7/20th Cup (35g)',
40: '2/5th Cup (40g)',
45: '9/20th Cup (45g)',
50: '1/2 Cup (50g)'
50: '1/2 Cup (50g)',
}
4 changes: 2 additions & 2 deletions custom_components/petkit/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/RobertD502/home-assistant-petkit/issues",
"requirements": ["petkitaio==0.1.5", "tzlocal>=4.2"],
"version": "0.1.5"
"requirements": ["petkitaio==0.1.6", "tzlocal>=4.2"],
"version": "0.1.6"
}
116 changes: 116 additions & 0 deletions custom_components/petkit/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ async def async_setup_entry(
MinEatingDuration(coordinator, feeder_id)
)

# Fresh Element Feeder
if feeder_data.type == 'feeder':
numbers.append(
FreshElementManualFeed(coordinator, feeder_id)
)

for lb_id, lb_data in coordinator.data.litter_boxes.items():
# Pura X & Pura MAX
numbers.append(
Expand Down Expand Up @@ -742,3 +748,113 @@ async def async_set_native_value(self, value: int) -> None:
self.feeder_data.data['settings']['shortest'] = value
self.async_write_ha_state()
await self.coordinator.async_request_refresh()


class FreshElementManualFeed(CoordinatorEntity, NumberEntity):
"""Representation of Fresh Element 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 has_entity_name(self) -> bool:
"""Indicate that entity has name defined."""

return True

@property
def translation_key(self) -> str:
"""Translation key for this entity."""

return "manual_feed"

@property
def icon(self) -> str:
"""Set icon."""

return 'mdi:bowl-mix'

@property
def native_value(self) -> int:
"""Returns lowest amount allowed."""

return 0

@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 0

@property
def native_max_value(self) -> int:
"""Return max value allowed."""

return 400

@property
def native_step(self) -> int:
"""Return stepping by 1."""

return 20

@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 < 20) or (value > 400):
raise PetKitError(f'{self.feeder_data.data["name"]} can only accept manual feeding amounts between 20 to 400 grams')
else:
await self.coordinator.client.manual_feeding(self.feeder_data, int(value))
await self.coordinator.async_request_refresh()
Loading

0 comments on commit eb14d8e

Please sign in to comment.