diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 6a85780ce78..672f8f10e2f 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -133,6 +133,11 @@ ENTITY_DESC_KEY_TARGET_TEMPERATURE = "target_temperature" ENTITY_DESC_KEY_MEASUREMENT = "measurement" ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing" +ENTITY_DESC_KEY_ENERGY_PRODUCTION_POWER = "energy_production_power" +ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME = "energy_production_time" +ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL = "energy_production_total" +ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY = "energy_production_today" + # This API key is only for use with Home Assistant. Reach out to Z-Wave JS to apply for # your own (https://github.com/zwave-js/firmware-updates/). API_KEY_FIRMWARE_UPDATE_SERVICE = ( diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index c17cbace657..62fc665c72e 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -722,9 +722,10 @@ DISCOVERY_SCHEMAS = [ hint="numeric_sensor", primary_value=ZWaveValueDiscoverySchema( command_class={ - CommandClass.SENSOR_MULTILEVEL, - CommandClass.SENSOR_ALARM, CommandClass.BATTERY, + CommandClass.ENERGY_PRODUCTION, + CommandClass.SENSOR_ALARM, + CommandClass.SENSOR_MULTILEVEL, }, type={ValueType.NUMBER}, ), diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 17a6e7c7522..e5a19f9bf2e 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -7,6 +7,14 @@ import logging from typing import Any, cast from zwave_js_server.const import CommandClass +from zwave_js_server.const.command_class.energy_production import ( + EnergyProductionParameter, + EnergyProductionScaleType, + PowerScale, + TodaysProductionScale, + TotalProductionScale, + TotalTimeScale, +) from zwave_js_server.const.command_class.meter import ( CURRENT_METER_TYPES, ENERGY_TOTAL_INCREASING_METER_TYPES, @@ -85,6 +93,10 @@ from zwave_js_server.model.value import ( Value as ZwaveValue, get_value_id_str, ) +from zwave_js_server.util.command_class.energy_production import ( + get_energy_production_parameter, + get_energy_production_scale_type, +) from zwave_js_server.util.command_class.meter import get_meter_scale_type from zwave_js_server.util.command_class.multilevel_sensor import ( get_multilevel_sensor_scale_type, @@ -123,6 +135,10 @@ from .const import ( ENTITY_DESC_KEY_CO2, ENTITY_DESC_KEY_CURRENT, ENTITY_DESC_KEY_ENERGY_MEASUREMENT, + ENTITY_DESC_KEY_ENERGY_PRODUCTION_POWER, + 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, @@ -138,6 +154,18 @@ from .const import ( ) from .helpers import ZwaveValueID +ENERGY_PRODUCTION_DEVICE_CLASS_MAP: dict[str, list[EnergyProductionParameter]] = { + ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME: [EnergyProductionParameter.TOTAL_TIME], + ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY: [ + EnergyProductionParameter.TODAYS_PRODUCTION + ], + ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL: [ + EnergyProductionParameter.TOTAL_PRODUCTION + ], + ENTITY_DESC_KEY_ENERGY_PRODUCTION_POWER: [EnergyProductionParameter.POWER], +} + + METER_DEVICE_CLASS_MAP: dict[str, list[MeterScaleType]] = { ENTITY_DESC_KEY_CURRENT: CURRENT_METER_TYPES, ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_METER_TYPES, @@ -160,6 +188,16 @@ MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, list[MultilevelSensorType]] = { ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_SENSORS, } +ENERGY_PRODUCTION_UNIT_MAP: dict[str, list[EnergyProductionScaleType]] = { + UnitOfEnergy.WATT_HOUR: [ + TotalProductionScale.WATT_HOURS, + TodaysProductionScale.WATT_HOURS, + ], + UnitOfPower.WATT: [PowerScale.WATTS], + UnitOfTime.SECONDS: [TotalTimeScale.SECONDS], + UnitOfTime.HOURS: [TotalTimeScale.HOURS], +} + METER_UNIT_MAP: dict[str, list[MeterScaleType]] = { UnitOfElectricCurrent.AMPERE: METER_UNIT_AMPERE, UnitOfVolume.CUBIC_FEET: UNIT_CUBIC_FEET, @@ -320,12 +358,18 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): @staticmethod def find_key_from_matching_set( - enum_value: MultilevelSensorType | MultilevelSensorScaleType | MeterScaleType, + enum_value: MultilevelSensorType + | MultilevelSensorScaleType + | MeterScaleType + | EnergyProductionParameter + | EnergyProductionScaleType, set_map: Mapping[ str, list[MultilevelSensorType] | list[MultilevelSensorScaleType] - | list[MeterScaleType], + | list[MeterScaleType] + | list[EnergyProductionScaleType] + | list[EnergyProductionParameter], ], ) -> str | None: """Find a key in a set map that matches a given enum value.""" @@ -387,6 +431,18 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): if key: return NumericSensorDataTemplateData(key, unit) + if value.command_class == CommandClass.ENERGY_PRODUCTION: + energy_production_parameter = get_energy_production_parameter(value) + energy_production_scale_type = get_energy_production_scale_type(value) + unit = self.find_key_from_matching_set( + energy_production_scale_type, ENERGY_PRODUCTION_UNIT_MAP + ) + key = self.find_key_from_matching_set( + energy_production_parameter, ENERGY_PRODUCTION_DEVICE_CLASS_MAP + ) + if key: + return NumericSensorDataTemplateData(key, unit) + return NumericSensorDataTemplateData() diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 074bfa212db..234062111f0 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -58,6 +58,10 @@ from .const import ( ENTITY_DESC_KEY_CO2, ENTITY_DESC_KEY_CURRENT, ENTITY_DESC_KEY_ENERGY_MEASUREMENT, + ENTITY_DESC_KEY_ENERGY_PRODUCTION_POWER, + 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, @@ -235,6 +239,50 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[ device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, ), + ( + ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, + UnitOfTime.SECONDS, + ): SensorEntityDescription( + 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( + 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( + 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( + 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, + ), + ( + ENTITY_DESC_KEY_ENERGY_PRODUCTION_POWER, + UnitOfPower.WATT, + ): SensorEntityDescription( + ENTITY_DESC_KEY_POWER, + name="Energy production power", + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfPower.WATT, + ), } # These descriptions are without device class. @@ -547,13 +595,14 @@ class ZwaveSensor(ZWaveBaseEntity, SensorEntity): unit_of_measurement: str | None = None, ) -> None: """Initialize a ZWaveSensorBase entity.""" - super().__init__(config_entry, driver, info) 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 - self._attr_name = self.generate_name(include_value_name=True) + if not entity_description.name: + self._attr_name = self.generate_name(include_value_name=True) @property def native_value(self) -> StateType: diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index af23fbef32e..860e5742c80 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -624,6 +624,12 @@ def indicator_test_state_fixture(): return json.loads(load_fixture("zwave_js/indicator_test_state.json")) +@pytest.fixture(name="energy_production_state", scope="session") +def energy_production_state_fixture(): + """Load a mock node with energy production CC state fixture data.""" + return json.loads(load_fixture("zwave_js/energy_production_state.json")) + + # model fixtures @@ -1191,3 +1197,11 @@ def indicator_test_fixture(client, indicator_test_state): node = Node(client, copy.deepcopy(indicator_test_state)) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="energy_production") +def energy_prodution_fixture(client, energy_production_state): + """Mock a mock node with Energy Production CC.""" + node = Node(client, copy.deepcopy(energy_production_state)) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/fixtures/energy_production_state.json b/tests/components/zwave_js/fixtures/energy_production_state.json new file mode 100644 index 00000000000..b074e51c499 --- /dev/null +++ b/tests/components/zwave_js/fixtures/energy_production_state.json @@ -0,0 +1,241 @@ +{ + "nodeId": 2, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 2, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 6, + "label": "Appliance" + }, + "specific": { + "key": 1, + "label": "General Appliance" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 144, + "name": "Energy Production", + "version": 1, + "isSecure": false + } + ] + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 1, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Library type", + "states": { + "0": "Unknown", + "1": "Static Controller", + "2": "Controller", + "3": "Enhanced Slave", + "4": "Slave", + "5": "Installer", + "6": "Routing Slave", + "7": "Bridge Controller", + "8": "Device under Test", + "9": "N/A", + "10": "AV Remote", + "11": "AV Device" + }, + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 1, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version", + "stateful": true, + "secret": false + } + }, + { + "endpoint": 0, + "commandClass": 144, + "commandClassName": "Energy Production", + "property": "value", + "propertyKey": 0, + "propertyName": "value", + "propertyKeyName": "0", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Power", + "ccSpecific": { + "parameter": 0, + "scale": 0 + }, + "unit": "W", + "stateful": true, + "secret": false + }, + "value": 1.23 + }, + { + "endpoint": 0, + "commandClass": 144, + "commandClassName": "Energy Production", + "property": "value", + "propertyKey": 1, + "propertyName": "value", + "propertyKeyName": "1", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Production Total", + "ccSpecific": { + "parameter": 1, + "scale": 0 + }, + "unit": "Wh", + "stateful": true, + "secret": false + }, + "value": 1234.56 + }, + { + "endpoint": 0, + "commandClass": 144, + "commandClassName": "Energy Production", + "property": "value", + "propertyKey": 2, + "propertyName": "value", + "propertyKeyName": "2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Production Today", + "ccSpecific": { + "parameter": 2, + "scale": 0 + }, + "unit": "Wh", + "stateful": true, + "secret": false + }, + "value": 123.45 + }, + { + "endpoint": 0, + "commandClass": 144, + "commandClassName": "Energy Production", + "property": "value", + "propertyKey": 3, + "propertyName": "value", + "propertyKeyName": "3", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Total Time", + "ccSpecific": { + "parameter": 3, + "scale": 0 + }, + "unit": "seconds", + "stateful": true, + "secret": false + }, + "value": 123456 + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 9600, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 6, + "label": "Appliance" + }, + "specific": { + "key": 1, + "label": "General Appliance" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "interviewStage": "Complete", + "statistics": { + "commandsTX": 10, + "commandsRX": 7, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 1, + "rtt": 84.8 + }, + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false +} diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index baf72f5da58..2bedb88884e 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -35,6 +35,7 @@ from homeassistant.const import ( UnitOfEnergy, UnitOfPower, UnitOfTemperature, + UnitOfTime, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -733,3 +734,54 @@ async def test_statistics_sensors( state = hass.states.get(f"{prefix}{suffix_key}") assert state assert state.state == str(val) + + +ENERGY_PRODUCTION_ENTITY_MAP = { + "energy_production_power": { + "state": 1.23, + "attributes": { + "unit_of_measurement": UnitOfPower.WATT, + "device_class": SensorDeviceClass.POWER, + "state_class": SensorStateClass.MEASUREMENT, + }, + }, + "energy_production_total": { + "state": 1234.56, + "attributes": { + "unit_of_measurement": UnitOfEnergy.WATT_HOUR, + "device_class": SensorDeviceClass.ENERGY, + "state_class": SensorStateClass.TOTAL_INCREASING, + }, + }, + "energy_production_today": { + "state": 123.45, + "attributes": { + "unit_of_measurement": UnitOfEnergy.WATT_HOUR, + "device_class": SensorDeviceClass.ENERGY, + "state_class": SensorStateClass.TOTAL_INCREASING, + }, + }, + "energy_production_time": { + "state": 123456.0, + "attributes": { + "unit_of_measurement": UnitOfTime.SECONDS, + "device_class": SensorDeviceClass.DURATION, + }, + "missing_attributes": ["state_class"], + }, +} + + +async def test_energy_production_sensors( + hass: HomeAssistant, energy_production, client, integration +) -> None: + """Test sensors for Energy Production CC.""" + for entity_id_suffix, state_data in ENERGY_PRODUCTION_ENTITY_MAP.items(): + state = hass.states.get(f"sensor.node_2_{entity_id_suffix}") + assert state + assert state.state == str(state_data["state"]) + for attr, val in state_data["attributes"].items(): + assert state.attributes[attr] == val + + for attr in state_data.get("missing_attributes", []): + assert attr not in state.attributes