Add support for Energy Production CC sensors (#93839)

This commit is contained in:
Raman Gupta 2023-05-31 07:28:07 -04:00 committed by GitHub
parent c72477811e
commit 1eb1ea08b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 424 additions and 6 deletions

View File

@ -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 = (

View File

@ -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},
),

View File

@ -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()

View File

@ -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:

View File

@ -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

View File

@ -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
}

View File

@ -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