mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
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:
parent
d47fe35a88
commit
5329a679bb
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user