"""Representation of Z-Wave sensors.""" from collections.abc import Callable, Mapping from dataclasses import dataclass from enum import IntEnum from typing import Any, cast import voluptuous as vol from zwave_js_server.const import CommandClass, RssiError from zwave_js_server.const.command_class.energy_production import ( CC_SPECIFIC_PARAMETER, CC_SPECIFIC_SCALE as ENERGY_PRODUCTION_CC_SPECIFIC_SCALE, EnergyProductionParameter, PowerScale, ) from zwave_js_server.const.command_class.meter import ( CC_SPECIFIC_METER_TYPE, CC_SPECIFIC_SCALE as METER_CC_SPECIFIC_SCALE, RESET_METER_OPTION_TARGET_VALUE, RESET_METER_OPTION_TYPE, VALUE_PROPERTY, ElectricScale, MeterType, ) from zwave_js_server.const.command_class.multilevel_sensor import ( CC_SPECIFIC_SCALE as MULTILEVEL_SENSOR_CC_SPECIFIC_SCALE, CC_SPECIFIC_SENSOR_TYPE, TEMPERATURE_SENSORS, TemperatureScale, ) from zwave_js_server.exceptions import ( BaseZwaveJSServerError, RssiErrorReceived, UnknownValueData, ) from zwave_js_server.model.controller import Controller from zwave_js_server.model.controller.statistics import ControllerStatistics from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.node.statistics import NodeStatistics from zwave_js_server.model.value import Value as ZwaveValue from zwave_js_server.util.command_class.energy_production import ( get_energy_production_scale_type, ) from zwave_js_server.util.command_class.meter import ( get_meter_scale_type, get_meter_type, ) from zwave_js_server.util.command_class.multilevel_sensor import ( get_multilevel_sensor_scale_type, ) from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, LIGHT_LUX, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, UV_INDEX, EntityCategory, Platform, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, UnitOfPower, UnitOfPressure, UnitOfTemperature, UnitOfTime, ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import UNDEFINED, StateType from .binary_sensor import is_valid_notification_binary_sensor from .const import ( ATTR_METER_TYPE, ATTR_METER_TYPE_NAME, ATTR_VALUE, DOMAIN, ENTITY_DESC_KEY_BATTERY_LIST_STATE, ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, ENTITY_DESC_KEY_BATTERY_TEMPERATURE, ENTITY_DESC_KEY_CO, ENTITY_DESC_KEY_CO2, ENTITY_DESC_KEY_CURRENT, ENTITY_DESC_KEY_ENERGY_MEASUREMENT, ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY, ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL, ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING, ENTITY_DESC_KEY_HUMIDITY, ENTITY_DESC_KEY_ILLUMINANCE, ENTITY_DESC_KEY_MEASUREMENT, ENTITY_DESC_KEY_POWER, ENTITY_DESC_KEY_POWER_FACTOR, ENTITY_DESC_KEY_PRESSURE, ENTITY_DESC_KEY_SIGNAL_STRENGTH, ENTITY_DESC_KEY_TARGET_TEMPERATURE, ENTITY_DESC_KEY_TOTAL_INCREASING, ENTITY_DESC_KEY_UV_INDEX, ENTITY_DESC_KEY_VOLTAGE, LOGGER, SERVICE_RESET_METER, ) from .discovery_data_template import ( NumericSensorDataTemplate, NumericSensorDataTemplateData, ) from .entity import NewZwaveDiscoveryInfo, ZWaveBaseEntity, ZWaveNodeBaseEntity from .migrate import async_migrate_statistics_sensors from .models import ( NewZWaveDiscoverySchema, ValueType, ZwaveDiscoveryInfo, ZwaveJSConfigEntry, ZWaveValueDiscoverySchema, ) PARALLEL_UPDATES = 0 # These descriptions should have a non None unit of measurement. ENTITY_DESCRIPTION_KEY_UNIT_MAP: dict[tuple[str, str], SensorEntityDescription] = { (ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, PERCENTAGE): SensorEntityDescription( key=ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, entity_registry_enabled_default=False, ), ( ENTITY_DESC_KEY_BATTERY_TEMPERATURE, UnitOfTemperature.CELSIUS, ): SensorEntityDescription( key=ENTITY_DESC_KEY_BATTERY_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, entity_registry_enabled_default=False, ), (ENTITY_DESC_KEY_CURRENT, UnitOfElectricCurrent.AMPERE): SensorEntityDescription( key=ENTITY_DESC_KEY_CURRENT, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, ), (ENTITY_DESC_KEY_VOLTAGE, UnitOfElectricPotential.VOLT): SensorEntityDescription( key=ENTITY_DESC_KEY_VOLTAGE, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.VOLT, ), ( ENTITY_DESC_KEY_VOLTAGE, UnitOfElectricPotential.MILLIVOLT, ): SensorEntityDescription( key=ENTITY_DESC_KEY_VOLTAGE, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, ), ( ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING, UnitOfEnergy.KILO_WATT_HOUR, ): SensorEntityDescription( key=ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, ), (ENTITY_DESC_KEY_POWER, UnitOfPower.WATT): SensorEntityDescription( key=ENTITY_DESC_KEY_POWER, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, ), (ENTITY_DESC_KEY_POWER_FACTOR, PERCENTAGE): SensorEntityDescription( key=ENTITY_DESC_KEY_POWER_FACTOR, device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), (ENTITY_DESC_KEY_CO, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription( key=ENTITY_DESC_KEY_CO, device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, ), (ENTITY_DESC_KEY_CO2, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription( key=ENTITY_DESC_KEY_CO2, device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, ), (ENTITY_DESC_KEY_HUMIDITY, PERCENTAGE): SensorEntityDescription( key=ENTITY_DESC_KEY_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), (ENTITY_DESC_KEY_ILLUMINANCE, LIGHT_LUX): SensorEntityDescription( key=ENTITY_DESC_KEY_ILLUMINANCE, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=LIGHT_LUX, ), (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.KPA): SensorEntityDescription( key=ENTITY_DESC_KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.KPA, ), (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.PSI): SensorEntityDescription( key=ENTITY_DESC_KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.PSI, ), (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.INHG): SensorEntityDescription( key=ENTITY_DESC_KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.INHG, ), (ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.MMHG): SensorEntityDescription( key=ENTITY_DESC_KEY_PRESSURE, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPressure.MMHG, ), ( ENTITY_DESC_KEY_SIGNAL_STRENGTH, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ): SensorEntityDescription( key=ENTITY_DESC_KEY_SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ), ( ENTITY_DESC_KEY_TARGET_TEMPERATURE, UnitOfTemperature.CELSIUS, ): SensorEntityDescription( key=ENTITY_DESC_KEY_TARGET_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), ( ENTITY_DESC_KEY_TARGET_TEMPERATURE, UnitOfTemperature.FAHRENHEIT, ): SensorEntityDescription( key=ENTITY_DESC_KEY_TARGET_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, ), ( ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, UnitOfTime.SECONDS, ): SensorEntityDescription( key=ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, name="Energy production time", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, ), (ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, UnitOfTime.HOURS): SensorEntityDescription( key=ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.HOURS, ), ( ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY, UnitOfEnergy.WATT_HOUR, ): SensorEntityDescription( key=ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY, name="Energy production today", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, ), ( ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL, UnitOfEnergy.WATT_HOUR, ): SensorEntityDescription( key=ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL, name="Energy production total", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, ), } # These descriptions are without unit of measurement. ENTITY_DESCRIPTION_KEY_MAP = { ENTITY_DESC_KEY_BATTERY_LIST_STATE: SensorEntityDescription( key=ENTITY_DESC_KEY_BATTERY_LIST_STATE, device_class=SensorDeviceClass.ENUM, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), ENTITY_DESC_KEY_CO: SensorEntityDescription( key=ENTITY_DESC_KEY_CO, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription( key=ENTITY_DESC_KEY_ENERGY_MEASUREMENT, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription( key=ENTITY_DESC_KEY_HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription( key=ENTITY_DESC_KEY_ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription( key=ENTITY_DESC_KEY_POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription( key=ENTITY_DESC_KEY_SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_MEASUREMENT: SensorEntityDescription( key=ENTITY_DESC_KEY_MEASUREMENT, state_class=SensorStateClass.MEASUREMENT, ), ENTITY_DESC_KEY_TOTAL_INCREASING: SensorEntityDescription( key=ENTITY_DESC_KEY_TOTAL_INCREASING, state_class=SensorStateClass.TOTAL_INCREASING, ), ENTITY_DESC_KEY_UV_INDEX: SensorEntityDescription( key=ENTITY_DESC_KEY_UV_INDEX, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UV_INDEX, ), } def convert_nested_attr( statistics: ControllerStatistics | NodeStatistics, key: str ) -> Any: """Convert a string that represents a nested attr to a value.""" data = statistics for _key in key.split("."): if data is None: return None # type: ignore[unreachable] data = getattr(data, _key) return data @dataclass(frozen=True, kw_only=True) class ZWaveJSStatisticsSensorEntityDescription(SensorEntityDescription): """Class to represent a Z-Wave JS statistics sensor entity description.""" convert: Callable[[ControllerStatistics | NodeStatistics, str], Any] = getattr entity_registry_enabled_default: bool = False # Controller statistics descriptions ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [ ZWaveJSStatisticsSensorEntityDescription( key="messages_tx", translation_key="successful_messages", translation_placeholders={"direction": "TX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="messages_rx", translation_key="successful_messages", translation_placeholders={"direction": "RX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="messages_dropped_tx", translation_key="messages_dropped", translation_placeholders={"direction": "TX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="messages_dropped_rx", translation_key="messages_dropped", translation_placeholders={"direction": "RX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="nak", translation_key="nak", state_class=SensorStateClass.TOTAL ), ZWaveJSStatisticsSensorEntityDescription( key="can", translation_key="can", state_class=SensorStateClass.TOTAL ), ZWaveJSStatisticsSensorEntityDescription( key="timeout_ack", translation_key="timeout_ack", state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="timeout_response", translation_key="timeout_response", state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="timeout_callback", translation_key="timeout_callback", state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="background_rssi.channel_0.average", translation_key="avg_signal_noise", translation_placeholders={"channel": "0"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, convert=convert_nested_attr, ), ZWaveJSStatisticsSensorEntityDescription( key="background_rssi.channel_0.current", translation_key="signal_noise", translation_placeholders={"channel": "0"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, convert=convert_nested_attr, ), ZWaveJSStatisticsSensorEntityDescription( key="background_rssi.channel_1.average", translation_key="avg_signal_noise", translation_placeholders={"channel": "1"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, convert=convert_nested_attr, ), ZWaveJSStatisticsSensorEntityDescription( key="background_rssi.channel_1.current", translation_key="signal_noise", translation_placeholders={"channel": "1"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, convert=convert_nested_attr, ), ZWaveJSStatisticsSensorEntityDescription( key="background_rssi.channel_2.average", translation_key="avg_signal_noise", translation_placeholders={"channel": "2"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, convert=convert_nested_attr, ), ZWaveJSStatisticsSensorEntityDescription( key="background_rssi.channel_2.current", translation_key="signal_noise", translation_placeholders={"channel": "2"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, convert=convert_nested_attr, ), ZWaveJSStatisticsSensorEntityDescription( key="background_rssi.channel_3.average", translation_key="avg_signal_noise", translation_placeholders={"channel": "3"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, convert=convert_nested_attr, ), ZWaveJSStatisticsSensorEntityDescription( key="background_rssi.channel_3.current", translation_key="signal_noise", translation_placeholders={"channel": "3"}, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, convert=convert_nested_attr, ), ] CONTROLLER_STATISTICS_KEY_MAP: dict[str, str] = { "messages_tx": "messagesTX", "messages_rx": "messagesRX", "messages_dropped_tx": "messagesDroppedTX", "messages_dropped_rx": "messagesDroppedRX", "nak": "NAK", "can": "CAN", "timeout_ack": "timeoutAck", "timeout_response": "timeoutResponse", "timeout_callback": "timeoutCallback", "background_rssi.channel_0.average": "backgroundRSSI.channel0.average", "background_rssi.channel_0.current": "backgroundRSSI.channel0.current", "background_rssi.channel_1.average": "backgroundRSSI.channel1.average", "background_rssi.channel_1.current": "backgroundRSSI.channel1.current", "background_rssi.channel_2.average": "backgroundRSSI.channel2.average", "background_rssi.channel_2.current": "backgroundRSSI.channel2.current", "background_rssi.channel_3.average": "backgroundRSSI.channel3.average", "background_rssi.channel_3.current": "backgroundRSSI.channel3.current", } # Node statistics descriptions ENTITY_DESCRIPTION_NODE_STATISTICS_LIST = [ ZWaveJSStatisticsSensorEntityDescription( key="commands_rx", translation_key="successful_commands", translation_placeholders={"direction": "RX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="commands_tx", translation_key="successful_commands", translation_placeholders={"direction": "TX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="commands_dropped_rx", translation_key="commands_dropped", translation_placeholders={"direction": "RX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="commands_dropped_tx", translation_key="commands_dropped", translation_placeholders={"direction": "TX"}, state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="timeout_response", translation_key="timeout_response", state_class=SensorStateClass.TOTAL, ), ZWaveJSStatisticsSensorEntityDescription( key="rtt", translation_key="rtt", native_unit_of_measurement=UnitOfTime.MILLISECONDS, device_class=SensorDeviceClass.DURATION, state_class=SensorStateClass.MEASUREMENT, ), ZWaveJSStatisticsSensorEntityDescription( key="rssi", translation_key="signal_strength", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, ), ZWaveJSStatisticsSensorEntityDescription( key="last_seen", translation_key="last_seen", device_class=SensorDeviceClass.TIMESTAMP, ), ] NODE_STATISTICS_KEY_MAP: dict[str, str] = { "commands_rx": "commandsRX", "commands_tx": "commandsTX", "commands_dropped_rx": "commandsDroppedRX", "commands_dropped_tx": "commandsDroppedTX", "timeout_response": "timeoutResponse", "rtt": "rtt", "rssi": "rssi", "last_seen": "lastSeen", } def get_entity_description( data: NumericSensorDataTemplateData, ) -> SensorEntityDescription: """Return the entity description for the given data.""" data_description_key = data.entity_description_key or "" data_unit = data.unit_of_measurement or "" return ENTITY_DESCRIPTION_KEY_UNIT_MAP.get( (data_description_key, data_unit), ENTITY_DESCRIPTION_KEY_MAP.get( data_description_key, SensorEntityDescription( key="base_sensor", native_unit_of_measurement=data.unit_of_measurement ), ), ) async def async_setup_entry( hass: HomeAssistant, config_entry: ZwaveJSConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up Z-Wave sensor from config entry.""" client = config_entry.runtime_data.client driver = client.driver assert driver is not None # Driver is ready before platforms are loaded. @callback def async_add_sensor(info: ZwaveDiscoveryInfo | NewZwaveDiscoveryInfo) -> None: """Add Z-Wave Sensor.""" entities: list[ZWaveBaseEntity] = [] if info.platform_data: data: NumericSensorDataTemplateData = info.platform_data else: data = NumericSensorDataTemplateData() entity_description = get_entity_description(data) if isinstance(info, NewZwaveDiscoveryInfo) and ( entity_class := info.entity_class ) in (NewZWaveNumericSensor, NewZWaveMeterSensor): entities.append(entity_class(config_entry, driver, info)) elif isinstance(info, NewZwaveDiscoveryInfo): pass # other entity classes are not migrated yet elif info.platform_hint == "numeric_sensor": entities.append( ZWaveNumericSensor( config_entry, driver, info, entity_description, data.unit_of_measurement, ) ) elif info.platform_hint == "notification": # prevent duplicate entities for values that are # already represented as binary sensors if is_valid_notification_binary_sensor(info): return entities.append( ZWaveListSensor(config_entry, driver, info, entity_description) ) elif info.platform_hint == "list": entities.append( ZWaveListSensor(config_entry, driver, info, entity_description) ) elif info.platform_hint == "config_parameter": entities.append( ZWaveConfigParameterSensor( config_entry, driver, info, entity_description ) ) elif info.platform_hint == "meter": entities.append( ZWaveMeterSensor(config_entry, driver, info, entity_description) ) else: entities.append(ZwaveSensor(config_entry, driver, info, entity_description)) async_add_entities(entities) @callback def async_add_controller_status_sensor() -> None: """Add controller status sensor.""" async_add_entities([ZWaveControllerStatusSensor(config_entry, driver)]) @callback def async_add_node_status_sensor(node: ZwaveNode) -> None: """Add node status sensor.""" async_add_entities([ZWaveNodeStatusSensor(config_entry, driver, node)]) @callback def async_add_statistics_sensors(node: ZwaveNode) -> None: """Add statistics sensors.""" async_migrate_statistics_sensors( hass, driver, node, CONTROLLER_STATISTICS_KEY_MAP if driver.controller.own_node == node else NODE_STATISTICS_KEY_MAP, ) async_add_entities( [ ZWaveStatisticsSensor( config_entry, driver, driver.controller if driver.controller.own_node == node else node, entity_description, ) for entity_description in ( ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST if driver.controller.own_node == node else ENTITY_DESCRIPTION_NODE_STATISTICS_LIST ) ] ) config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_{SENSOR_DOMAIN}", async_add_sensor, ) ) config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_controller_status_sensor", async_add_controller_status_sensor, ) ) config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_node_status_sensor", async_add_node_status_sensor, ) ) config_entry.async_on_unload( async_dispatcher_connect( hass, f"{DOMAIN}_{config_entry.entry_id}_add_statistics_sensors", async_add_statistics_sensors, ) ) platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_RESET_METER, { vol.Optional(ATTR_METER_TYPE): vol.Coerce(int), vol.Optional(ATTR_VALUE): vol.Coerce(int), }, "async_reset_meter", ) class ZwaveSensor(ZWaveBaseEntity, SensorEntity): """Basic Representation of a Z-Wave sensor.""" def __init__( self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, entity_description: SensorEntityDescription, unit_of_measurement: str | None = None, ) -> None: """Initialize a ZWaveSensorBase entity.""" self.entity_description = entity_description super().__init__(config_entry, driver, info) self._attr_native_unit_of_measurement = unit_of_measurement # Entity class attributes self._attr_force_update = True if not entity_description.name or entity_description.name is UNDEFINED: self._attr_name = self.generate_name(include_value_name=True) @property def native_value(self) -> StateType: """Return state of the sensor.""" key = str(self.info.primary_value.value) if key not in self.info.primary_value.metadata.states: return self.info.primary_value.value return str(self.info.primary_value.metadata.states[key]) @property def native_unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" if (unit := super().native_unit_of_measurement) is not None: return unit if self.info.primary_value.metadata.unit is None: return None return str(self.info.primary_value.metadata.unit) class ZWaveNumericSensor(ZwaveSensor): """Representation of a Z-Wave Numeric sensor.""" def __init__( self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, entity_description: SensorEntityDescription, unit_of_measurement: str | None = None, ) -> None: """Initialize a ZWaveBasicSensor entity.""" super().__init__( config_entry, driver, info, entity_description, unit_of_measurement ) if self.info.primary_value.command_class == CommandClass.BASIC: self._attr_name = self.generate_name( include_value_name=True, alternate_value_name="Basic" ) @callback def on_value_update(self) -> None: """Handle scale changes for this value on value updated event.""" data = NumericSensorDataTemplate().resolve_data(self.info.primary_value) self.entity_description = get_entity_description(data) self._attr_native_unit_of_measurement = data.unit_of_measurement @property def native_value(self) -> float | None: """Return state of the sensor.""" if self.info.primary_value.value is None: return None return float(self.info.primary_value.value) class NewZWaveNumericSensor(ZWaveBaseEntity, SensorEntity): """Representation of a Z-Wave Numeric sensor.""" _attr_force_update = True def __init__( self, config_entry: ConfigEntry, driver: Driver, info: NewZwaveDiscoveryInfo, ) -> None: """Initialize the entity.""" super().__init__(config_entry, driver, info) entity_description = info.entity_description if not entity_description.name or entity_description.name is UNDEFINED: self._attr_name = self.generate_name(include_value_name=True) self._scale_type = self._get_scale_type() def _get_scale_type(self) -> IntEnum | None: """Return the scale type of the value.""" primary_value = self.info.primary_value scale_type_function: Callable[[ZwaveValue], IntEnum] | None match primary_value.command_class: case CommandClass.METER: scale_type_function = get_meter_scale_type case CommandClass.SENSOR_MULTILEVEL: scale_type_function = get_multilevel_sensor_scale_type case CommandClass.ENERGY_PRODUCTION: scale_type_function = get_energy_production_scale_type case _: scale_type_function = None if scale_type_function is None: return None try: scale_type = scale_type_function(primary_value) except UnknownValueData: return None return scale_type @callback def on_value_update(self) -> None: """Handle scale changes for this value on value updated event.""" # TODO: Try to limit this to metadata updated event. # pylint: disable=fixme scale_type = self._get_scale_type() if scale_type is not self._scale_type: self.hass.config_entries.async_schedule_reload(self.config_entry.entry_id) @property def native_value(self) -> float | None: """Return state of the sensor.""" if self.info.primary_value.value is None: return None return float(self.info.primary_value.value) class ZWaveMeterSensor(ZWaveNumericSensor): """Representation of a Z-Wave Meter CC sensor.""" @property def extra_state_attributes(self) -> Mapping[str, int | str] | None: """Return extra state attributes.""" meter_type = get_meter_type(self.info.primary_value) return { ATTR_METER_TYPE: meter_type.value, ATTR_METER_TYPE_NAME: meter_type.name, } async def async_reset_meter( self, meter_type: int | None = None, value: int | None = None ) -> None: """Reset meter(s) on device.""" node = self.info.node endpoint = self.info.primary_value.endpoint or 0 options = {} if meter_type is not None: options[RESET_METER_OPTION_TYPE] = meter_type if value is not None: options[RESET_METER_OPTION_TARGET_VALUE] = value args = [options] if options else [] try: await node.endpoints[endpoint].async_invoke_cc_api( CommandClass.METER, "reset", *args, wait_for_result=False ) except BaseZwaveJSServerError as err: raise HomeAssistantError( f"Failed to reset meters on node {node} endpoint {endpoint}: {err}" ) from err LOGGER.debug( "Meters on node %s endpoint %s reset with the following options: %s", node, endpoint, options, ) class NewZWaveMeterSensor(NewZWaveNumericSensor): """Representation of a Z-Wave Meter CC sensor.""" @property def extra_state_attributes(self) -> Mapping[str, int | str] | None: """Return extra state attributes.""" meter_type = get_meter_type(self.info.primary_value) return { ATTR_METER_TYPE: meter_type.value, ATTR_METER_TYPE_NAME: meter_type.name, } async def async_reset_meter( self, meter_type: int | None = None, value: int | None = None ) -> None: """Reset meter(s) on device.""" node = self.info.node endpoint = self.info.primary_value.endpoint or 0 options = {} if meter_type is not None: options[RESET_METER_OPTION_TYPE] = meter_type if value is not None: options[RESET_METER_OPTION_TARGET_VALUE] = value args = [options] if options else [] try: await node.endpoints[endpoint].async_invoke_cc_api( CommandClass.METER, "reset", *args, wait_for_result=False ) except BaseZwaveJSServerError as err: raise HomeAssistantError( f"Failed to reset meters on node {node} endpoint {endpoint}: {err}" ) from err LOGGER.debug( "Meters on node %s endpoint %s reset with the following options: %s", node, endpoint, options, ) class ZWaveListSensor(ZwaveSensor): """Representation of a Z-Wave Numeric sensor with multiple states.""" def __init__( self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, entity_description: SensorEntityDescription, unit_of_measurement: str | None = None, ) -> None: """Initialize a ZWaveListSensor entity.""" super().__init__( config_entry, driver, info, entity_description, unit_of_measurement ) # Entity class attributes # Notification sensors use the notification event label as the name # (property_key_name/metadata.label, falling back to property_name) # https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/notifications.json if info.platform_hint == "notification": self._attr_name = self.generate_name( alternate_value_name=( info.primary_value.property_key_name or info.primary_value.metadata.label or info.primary_value.property_name ) ) else: self._attr_name = self.generate_name( alternate_value_name=info.primary_value.property_name, additional_info=[info.primary_value.property_key_name], ) if self.info.primary_value.metadata.states: self._attr_device_class = SensorDeviceClass.ENUM self._attr_options = list(info.primary_value.metadata.states.values()) @callback def should_rediscover_on_metadata_update(self) -> bool: """Check if metadata states have changed.""" return list(self.info.primary_value.metadata.states.values()) != ( self._attr_options or [] ) @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" if (value := self.info.primary_value.value) is None: return None # add the value's int value as property for multi-value (list) items return {ATTR_VALUE: value} class ZWaveConfigParameterSensor(ZWaveListSensor): """Representation of a Z-Wave config parameter sensor.""" _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, config_entry: ZwaveJSConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo, entity_description: SensorEntityDescription, unit_of_measurement: str | None = None, ) -> None: """Initialize a ZWaveConfigParameterSensor entity.""" super().__init__( config_entry, driver, info, entity_description, unit_of_measurement ) property_key_name = self.info.primary_value.property_key_name # Entity class attributes self._attr_name = self.generate_name( alternate_value_name=self.info.primary_value.property_name, additional_info=[property_key_name] if property_key_name else None, ) @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" if (value := self.info.primary_value.value) is None: return None # add the value's int value as property for multi-value (list) items return {ATTR_VALUE: value} class ZWaveNodeStatusSensor(ZWaveNodeBaseEntity, SensorEntity): """Representation of a node status sensor.""" _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_translation_key = "node_status" def __init__( self, config_entry: ZwaveJSConfigEntry, driver: Driver, node: ZwaveNode ) -> None: """Initialize a generic Z-Wave device entity.""" super().__init__(driver, node) self.config_entry = config_entry self._attr_unique_id = f"{self._base_unique_id}.node_status" @callback def _status_changed(self, _: dict) -> None: """Call when status event is received.""" self._attr_native_value = self.node.status.name.lower() self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Call when entity is added.""" await super().async_added_to_hass() for evt in ("wake up", "sleep", "dead", "alive"): self.async_on_remove(self.node.on(evt, self._status_changed)) self._attr_native_value: str = self.node.status.name.lower() self.async_write_ha_state() class ZWaveControllerStatusSensor(ZWaveNodeBaseEntity, SensorEntity): """Representation of a controller status sensor.""" _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_translation_key = "controller_status" def __init__(self, config_entry: ZwaveJSConfigEntry, driver: Driver) -> None: """Initialize a generic Z-Wave device entity.""" self.controller = driver.controller node = self.controller.own_node assert node super().__init__(driver, node) self.config_entry = config_entry self._attr_unique_id = f"{self._base_unique_id}.controller_status" @callback def _status_changed(self, _: dict) -> None: """Call when status event is received.""" self._attr_native_value = self.controller.status.name.lower() self.async_write_ha_state() async def async_added_to_hass(self) -> None: """Call when entity is added.""" await super().async_added_to_hass() self.async_on_remove(self.controller.on("status changed", self._status_changed)) self._attr_native_value: str = self.controller.status.name.lower() class ZWaveStatisticsSensor(ZWaveNodeBaseEntity, SensorEntity): """Representation of a node/controller statistics sensor.""" entity_description: ZWaveJSStatisticsSensorEntityDescription _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, config_entry: ZwaveJSConfigEntry, driver: Driver, statistics_src: Controller | ZwaveNode, description: ZWaveJSStatisticsSensorEntityDescription, ) -> None: """Initialize a Z-Wave statistics entity.""" self.statistics_src = statistics_src node = ( statistics_src.own_node if isinstance(statistics_src, Controller) else statistics_src ) assert node super().__init__(driver, node) self.entity_description = description self.config_entry = config_entry self._attr_unique_id = f"{self._base_unique_id}.statistics_{description.key}" @callback def _statistics_updated(self, event_data: dict) -> None: """Call when statistics updated event is received.""" statistics = cast( ControllerStatistics | NodeStatistics, event_data["statistics_updated"] ) self._set_statistics(statistics) self.async_write_ha_state() @callback def _set_statistics( self, statistics: ControllerStatistics | NodeStatistics ) -> None: """Set updated statistics.""" try: self._attr_native_value = self.entity_description.convert( statistics, self.entity_description.key ) except RssiErrorReceived as err: if err.error is RssiError.NOT_AVAILABLE: self._attr_available = False return self._attr_native_value = None # Reset available state. self._attr_available = True async def async_added_to_hass(self) -> None: """Call when entity is added.""" await super().async_added_to_hass() self.async_on_remove( self.statistics_src.on("statistics updated", self._statistics_updated) ) # Set initial state self._set_statistics(self.statistics_src.statistics) DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [ NewZWaveDiscoverySchema( platform=Platform.SENSOR, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.BATTERY}, type={ValueType.NUMBER}, property={"level"}, ), entity_class=NewZWaveNumericSensor, entity_description=SensorEntityDescription( key="battery_level", device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), ), NewZWaveDiscoverySchema( platform=Platform.SENSOR, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.SENSOR_MULTILEVEL}, type={ValueType.NUMBER}, all_available_cc_specific={ (MULTILEVEL_SENSOR_CC_SPECIFIC_SCALE, TemperatureScale.CELSIUS), }, any_available_cc_specific={ (CC_SPECIFIC_SENSOR_TYPE, sensor_type) for sensor_type in TEMPERATURE_SENSORS }, ), entity_class=NewZWaveNumericSensor, entity_description=SensorEntityDescription( key="temperature_celsius", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), ), NewZWaveDiscoverySchema( platform=Platform.SENSOR, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.SENSOR_MULTILEVEL}, type={ValueType.NUMBER}, all_available_cc_specific={ (MULTILEVEL_SENSOR_CC_SPECIFIC_SCALE, TemperatureScale.FAHRENHEIT), }, any_available_cc_specific={ (CC_SPECIFIC_SENSOR_TYPE, sensor_type) for sensor_type in TEMPERATURE_SENSORS }, ), entity_class=NewZWaveNumericSensor, entity_description=SensorEntityDescription( key="temperature_fahrenheit", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, ), ), NewZWaveDiscoverySchema( platform=Platform.SENSOR, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.METER}, type={ValueType.NUMBER}, property={VALUE_PROPERTY}, all_available_cc_specific={ (CC_SPECIFIC_METER_TYPE, MeterType.ELECTRIC), (METER_CC_SPECIFIC_SCALE, ElectricScale.AMPERE), }, ), entity_class=NewZWaveMeterSensor, entity_description=SensorEntityDescription( key="meter_current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, ), ), NewZWaveDiscoverySchema( platform=Platform.SENSOR, primary_value=ZWaveValueDiscoverySchema( command_class={CommandClass.ENERGY_PRODUCTION}, type={ValueType.NUMBER}, all_available_cc_specific={ (CC_SPECIFIC_PARAMETER, EnergyProductionParameter.POWER), (ENERGY_PRODUCTION_CC_SPECIFIC_SCALE, PowerScale.WATTS), }, ), entity_class=NewZWaveNumericSensor, entity_description=SensorEntityDescription( key="energy_production_power", name="Energy production power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, ), ), ]