Add Aqara c1 pet feeder support to ZHA (#82340)

* Add Aqara c1 pet feeder support to ZHA

* clean up

* cleanup

* state classes for daily measurements

* cleanups

* cleanups

* restore the refreshing of the inverted value cache

* cleanup
This commit is contained in:
David F. Mulcahey 2022-11-21 18:03:17 -05:00 committed by GitHub
parent d47fe35a88
commit 5329a679bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 165 additions and 10 deletions

View File

@ -194,3 +194,12 @@ class ReplaceFilter(BinarySensor, id_suffix="replace_filter"):
SENSOR_ATTR = "replace_filter" SENSOR_ATTR = "replace_filter"
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM
@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
class AqaraPetFeederErrorDetected(BinarySensor, id_suffix="error_detected"):
"""ZHA aqara pet feeder error detected binary sensor."""
SENSOR_ATTR = "error_detected"
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM
_attr_name: str = "Error detected"

View File

@ -177,3 +177,12 @@ class NoPresenceStatusResetButton(
_attribute_value = 1 _attribute_value = 1
_attr_device_class = ButtonDeviceClass.RESTART _attr_device_class = ButtonDeviceClass.RESTART
_attr_entity_category = EntityCategory.CONFIG _attr_entity_category = EntityCategory.CONFIG
@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
class AqaraPetFeederFeedButton(ZHAAttributeButton, id_suffix="feeding"):
"""Defines a feed button for the aqara c1 pet feeder."""
_attribute_name = "feeding"
_attr_name = "Feed"
_attribute_value = 1

View File

@ -126,6 +126,17 @@ class OppleRemote(ZigbeeChannel):
self.ZCL_INIT_ATTRS = { self.ZCL_INIT_ATTRS = {
"power_outage_memory": True, "power_outage_memory": True,
} }
elif self.cluster.endpoint.model == "aqara.feeder.acn001":
self.ZCL_INIT_ATTRS = {
"portions_dispensed": True,
"weight_dispensed": True,
"error_detected": True,
"disable_led_indicator": True,
"child_lock": True,
"feeding_mode": True,
"serving_size": True,
"portion_weight": True,
}
async def async_initialize_channel_specific(self, from_cache: bool) -> None: async def async_initialize_channel_specific(self, from_cache: bool) -> None:
"""Initialize channel specific.""" """Initialize channel specific."""

View File

@ -8,9 +8,9 @@ from typing import TYPE_CHECKING, Any, TypeVar
import zigpy.exceptions import zigpy.exceptions
from zigpy.zcl.foundation import Status from zigpy.zcl.foundation import Status
from homeassistant.components.number import NumberEntity from homeassistant.components.number import NumberEntity, NumberMode
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform, UnitOfMass
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
@ -836,3 +836,32 @@ class InovelliDefaultAllLEDOffIntensity(
_attr_native_max_value: float = 100 _attr_native_max_value: float = 100
_zcl_attribute: str = "led_intensity_when_off" _zcl_attribute: str = "led_intensity_when_off"
_attr_name: str = "Default all LED off intensity" _attr_name: str = "Default all LED off intensity"
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
class AqaraPetFeederServingSize(ZHANumberConfigurationEntity, id_suffix="serving_size"):
"""Aqara pet feeder serving size configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_native_min_value: float = 1
_attr_native_max_value: float = 10
_zcl_attribute: str = "serving_size"
_attr_name: str = "Serving to dispense"
_attr_mode: NumberMode = NumberMode.BOX
_attr_icon: str = "mdi:counter"
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
class AqaraPetFeederPortionWeight(
ZHANumberConfigurationEntity, id_suffix="portion_weight"
):
"""Aqara pet feeder portion weight configuration entity."""
_attr_entity_category = EntityCategory.CONFIG
_attr_native_min_value: float = 1
_attr_native_max_value: float = 100
_zcl_attribute: str = "portion_weight"
_attr_name: str = "Portion weight"
_attr_mode: NumberMode = NumberMode.BOX
_attr_native_unit_of_measurement: str = UnitOfMass.GRAMS
_attr_icon: str = "mdi:weight-gram"

View File

@ -477,3 +477,20 @@ class InovelliSwitchTypeEntity(ZCLEnumSelectEntity, id_suffix="switch_type"):
_select_attr = "switch_type" _select_attr = "switch_type"
_enum = InovelliSwitchType _enum = InovelliSwitchType
_attr_name: str = "Switch type" _attr_name: str = "Switch type"
class AqaraFeedingMode(types.enum8):
"""Feeding mode."""
Manual = 0x00
Schedule = 0x01
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
class AqaraPetFeederMode(ZCLEnumSelectEntity, id_suffix="feeding_mode"):
"""Representation of an Aqara pet feeder mode configuration entity."""
_select_attr = "feeding_mode"
_enum = AqaraFeedingMode
_attr_name = "Mode"
_attr_icon: str = "mdi:wrench-clock"

View File

@ -5,6 +5,8 @@ import functools
import numbers import numbers
from typing import TYPE_CHECKING, Any, TypeVar from typing import TYPE_CHECKING, Any, TypeVar
from zigpy import types
from homeassistant.components.climate import HVACAction from homeassistant.components.climate import HVACAction
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -36,6 +38,7 @@ from homeassistant.const import (
VOLUME_GALLONS, VOLUME_GALLONS,
VOLUME_LITERS, VOLUME_LITERS,
Platform, Platform,
UnitOfMass,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -837,3 +840,53 @@ class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"):
_attr_icon = "mdi:timer" _attr_icon = "mdi:timer"
_attr_name: str = "Filter run time" _attr_name: str = "Filter run time"
_unit = TIME_MINUTES _unit = TIME_MINUTES
class AqaraFeedingSource(types.enum8):
"""Aqara pet feeder feeding source."""
Feeder = 0x01
HomeAssistant = 0x02
@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
class AqaraPetFeederLastFeedingSource(Sensor, id_suffix="last_feeding_source"):
"""Sensor that displays the last feeding source of pet feeder."""
SENSOR_ATTR = "last_feeding_source"
_attr_name: str = "Last feeding source"
_attr_icon = "mdi:devices"
def formatter(self, value: int) -> int | float | None:
"""Numeric pass-through formatter."""
return AqaraFeedingSource(value).name
@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
class AqaraPetFeederLastFeedingSize(Sensor, id_suffix="last_feeding_size"):
"""Sensor that displays the last feeding size of the pet feeder."""
SENSOR_ATTR = "last_feeding_size"
_attr_name: str = "Last feeding size"
_attr_icon: str = "mdi:counter"
@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
class AqaraPetFeederPortionsDispensed(Sensor, id_suffix="portions_dispensed"):
"""Sensor that displays the number of portions dispensed by the pet feeder."""
SENSOR_ATTR = "portions_dispensed"
_attr_name: str = "Portions dispensed today"
_attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING
_attr_icon: str = "mdi:counter"
@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
class AqaraPetFeederWeightDispensed(Sensor, id_suffix="weight_dispensed"):
"""Sensor that displays the weight weight dispensed by the pet feeder."""
SENSOR_ATTR = "weight_dispensed"
_attr_name: str = "Weight dispensed today"
_unit = UnitOfMass.GRAMS
_attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING
_attr_icon: str = "mdi:weight-gram"

View File

@ -174,7 +174,8 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity):
_attr_entity_category = EntityCategory.CONFIG _attr_entity_category = EntityCategory.CONFIG
_zcl_attribute: str _zcl_attribute: str
_zcl_inverter_attribute: str = "" _zcl_inverter_attribute: str | None = None
_force_inverted: bool = False
@classmethod @classmethod
def create_entity( def create_entity(
@ -225,19 +226,24 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity):
"""Handle state update from channel.""" """Handle state update from channel."""
self.async_write_ha_state() self.async_write_ha_state()
@property
def inverted(self) -> bool:
"""Return True if the switch is inverted."""
if self._zcl_inverter_attribute:
return bool(self._channel.cluster.get(self._zcl_inverter_attribute))
return self._force_inverted
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return if the switch is on based on the statemachine.""" """Return if the switch is on based on the statemachine."""
val = bool(self._channel.cluster.get(self._zcl_attribute)) val = bool(self._channel.cluster.get(self._zcl_attribute))
invert = bool(self._channel.cluster.get(self._zcl_inverter_attribute)) return (not val) if self.inverted else val
return (not val) if invert else val
async def async_turn_on_off(self, state: bool) -> None: async def async_turn_on_off(self, state: bool) -> None:
"""Turn the entity on or off.""" """Turn the entity on or off."""
try: try:
invert = bool(self._channel.cluster.get(self._zcl_inverter_attribute))
result = await self._channel.cluster.write_attributes( result = await self._channel.cluster.write_attributes(
{self._zcl_attribute: not state if invert else state} {self._zcl_attribute: not state if self.inverted else state}
) )
except zigpy.exceptions.ZigbeeException as ex: except zigpy.exceptions.ZigbeeException as ex:
self.error("Could not set value: %s", ex) self.error("Could not set value: %s", ex)
@ -258,15 +264,15 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Attempt to retrieve the state of the entity.""" """Attempt to retrieve the state of the entity."""
await super().async_update() await super().async_update()
_LOGGER.error("Polling current state") self.error("Polling current state")
if self._channel: if self._channel:
value = await self._channel.get_attribute_value( value = await self._channel.get_attribute_value(
self._zcl_attribute, from_cache=False self._zcl_attribute, from_cache=False
) )
invert = await self._channel.get_attribute_value( await self._channel.get_attribute_value(
self._zcl_inverter_attribute, from_cache=False self._zcl_inverter_attribute, from_cache=False
) )
_LOGGER.debug("read value=%s, inverter=%s", value, bool(invert)) self.debug("read value=%s, inverted=%s", value, self.inverted)
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
@ -430,3 +436,24 @@ class InovelliDisableDoubleTapClearNotificationsMode(
_zcl_attribute: str = "disable_clear_notifications_double_tap" _zcl_attribute: str = "disable_clear_notifications_double_tap"
_attr_name: str = "Disable config 2x tap to clear notifications" _attr_name: str = "Disable config 2x tap to clear notifications"
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
class AqaraPetFeederLEDIndicator(
ZHASwitchConfigurationEntity, id_suffix="disable_led_indicator"
):
"""Representation of a LED indicator configuration entity."""
_zcl_attribute: str = "disable_led_indicator"
_attr_name = "LED indicator"
_force_inverted = True
_attr_icon: str = "mdi:led-on"
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
class AqaraPetFeederChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
"""Representation of a child lock configuration entity."""
_zcl_attribute: str = "child_lock"
_attr_name = "Child lock"
_attr_icon: str = "mdi:account-lock"