diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index abddfda3358..1250e3c92a7 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -194,3 +194,12 @@ class ReplaceFilter(BinarySensor, id_suffix="replace_filter"): SENSOR_ATTR = "replace_filter" _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" diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 41f3846e97f..e41e7a81e12 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -177,3 +177,12 @@ class NoPresenceStatusResetButton( _attribute_value = 1 _attr_device_class = ButtonDeviceClass.RESTART _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 diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index b60df5d3706..427579cfb59 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -126,6 +126,17 @@ class OppleRemote(ZigbeeChannel): self.ZCL_INIT_ATTRS = { "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: """Initialize channel specific.""" diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 8a71321c208..41e90899894 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -8,9 +8,9 @@ from typing import TYPE_CHECKING, Any, TypeVar import zigpy.exceptions 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.const import Platform +from homeassistant.const import Platform, UnitOfMass from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory @@ -836,3 +836,32 @@ class InovelliDefaultAllLEDOffIntensity( _attr_native_max_value: float = 100 _zcl_attribute: str = "led_intensity_when_off" _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" diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 96925550676..295c61314c7 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -477,3 +477,20 @@ class InovelliSwitchTypeEntity(ZCLEnumSelectEntity, id_suffix="switch_type"): _select_attr = "switch_type" _enum = InovelliSwitchType _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" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index ba4aec66f35..003f5771b93 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -5,6 +5,8 @@ import functools import numbers from typing import TYPE_CHECKING, Any, TypeVar +from zigpy import types + from homeassistant.components.climate import HVACAction from homeassistant.components.sensor import ( SensorDeviceClass, @@ -36,6 +38,7 @@ from homeassistant.const import ( VOLUME_GALLONS, VOLUME_LITERS, Platform, + UnitOfMass, ) from homeassistant.core import HomeAssistant, callback 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_name: str = "Filter run time" _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" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 0c2e5e7ebe2..ac285700a27 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -174,7 +174,8 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): _attr_entity_category = EntityCategory.CONFIG _zcl_attribute: str - _zcl_inverter_attribute: str = "" + _zcl_inverter_attribute: str | None = None + _force_inverted: bool = False @classmethod def create_entity( @@ -225,19 +226,24 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): """Handle state update from channel.""" 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 def is_on(self) -> bool: """Return if the switch is on based on the statemachine.""" val = bool(self._channel.cluster.get(self._zcl_attribute)) - invert = bool(self._channel.cluster.get(self._zcl_inverter_attribute)) - return (not val) if invert else val + return (not val) if self.inverted else val async def async_turn_on_off(self, state: bool) -> None: """Turn the entity on or off.""" try: - invert = bool(self._channel.cluster.get(self._zcl_inverter_attribute)) 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: self.error("Could not set value: %s", ex) @@ -258,15 +264,15 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): async def async_update(self) -> None: """Attempt to retrieve the state of the entity.""" await super().async_update() - _LOGGER.error("Polling current state") + self.error("Polling current state") if self._channel: value = await self._channel.get_attribute_value( 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 ) - _LOGGER.debug("read value=%s, inverter=%s", value, bool(invert)) + self.debug("read value=%s, inverted=%s", value, self.inverted) @CONFIG_DIAGNOSTIC_MATCH( @@ -430,3 +436,24 @@ class InovelliDisableDoubleTapClearNotificationsMode( _zcl_attribute: str = "disable_clear_notifications_double_tap" _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"