From 868a536d8175d0a56fb8766d1fb38571dfc43b52 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 24 Feb 2021 19:39:35 +0100 Subject: [PATCH] Add number platform to Z-Wave JS (#46956) * add number platform to zwave_js integration * add discovery scheme for thermostat valve control, using number platform Co-authored-by: kpine --- homeassistant/components/zwave_js/const.py | 1 + .../components/zwave_js/discovery.py | 8 + homeassistant/components/zwave_js/number.py | 84 +++ tests/components/zwave_js/conftest.py | 14 + tests/components/zwave_js/test_number.py | 69 ++ .../aeotec_radiator_thermostat_state.json | 626 ++++++++++++++++++ 6 files changed, 802 insertions(+) create mode 100644 homeassistant/components/zwave_js/number.py create mode 100644 tests/components/zwave_js/test_number.py create mode 100644 tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index dba4e6d33a3..19e6fc3db14 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -9,6 +9,7 @@ PLATFORMS = [ "fan", "light", "lock", + "number", "sensor", "switch", ] diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index f4f5c359e22..77709f84e58 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -365,6 +365,14 @@ DISCOVERY_SCHEMAS = [ device_class_specific={"Fan Switch"}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), + # number platform + # valve control for thermostats + ZWaveDiscoverySchema( + platform="number", + hint="Valve control", + device_class_generic={"Thermostat"}, + primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, + ), # lights # primary value is the currentValue (brightness) # catch any device with multilevel CC as light diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py new file mode 100644 index 00000000000..8f8e894cda2 --- /dev/null +++ b/homeassistant/components/zwave_js/number.py @@ -0,0 +1,84 @@ +"""Support for Z-Wave controls using the number platform.""" +from typing import Callable, List, Optional + +from zwave_js_server.client import Client as ZwaveClient + +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave Number entity from Config Entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_number(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave number entity.""" + entities: List[ZWaveBaseEntity] = [] + entities.append(ZwaveNumberEntity(config_entry, client, info)) + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{NUMBER_DOMAIN}", + async_add_number, + ) + ) + + +class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): + """Representation of a Z-Wave number entity.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZwaveNumberEntity entity.""" + super().__init__(config_entry, client, info) + self._name = self.generate_name( + include_value_name=True, alternate_value_name=info.platform_hint + ) + if self.info.primary_value.metadata.writeable: + self._target_value = self.info.primary_value + else: + self._target_value = self.get_zwave_value("targetValue") + + @property + def min_value(self) -> float: + """Return the minimum value.""" + if self.info.primary_value.metadata.min is None: + return 0 + return float(self.info.primary_value.metadata.min) + + @property + def max_value(self) -> float: + """Return the maximum value.""" + if self.info.primary_value.metadata.max is None: + return 255 + return float(self.info.primary_value.metadata.max) + + @property + def value(self) -> Optional[float]: # type: ignore + """Return the entity value.""" + if self.info.primary_value.value is None: + return None + return float(self.info.primary_value.value) + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return the unit of measurement of this entity, if any.""" + if self.info.primary_value.metadata.unit is None: + return None + return str(self.info.primary_value.metadata.unit) + + async def async_set_value(self, value: float) -> None: + """Set new value.""" + await self.info.node.async_set_value(self._target_value, value) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index a9618acc64d..e0bc588abf4 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -160,6 +160,12 @@ def ge_12730_state_fixture(): return json.loads(load_fixture("zwave_js/fan_ge_12730_state.json")) +@pytest.fixture(name="aeotec_radiator_thermostat_state", scope="session") +def aeotec_radiator_thermostat_state_fixture(): + """Load the Aeotec Radiator Thermostat node state fixture data.""" + return json.loads(load_fixture("zwave_js/aeotec_radiator_thermostat_state.json")) + + @pytest.fixture(name="client") def mock_client_fixture(controller_state, version_state): """Mock a client.""" @@ -295,6 +301,14 @@ def nortek_thermostat_fixture(client, nortek_thermostat_state): return node +@pytest.fixture(name="aeotec_radiator_thermostat") +def aeotec_radiator_thermostat_fixture(client, aeotec_radiator_thermostat_state): + """Mock a Aeotec thermostat node.""" + node = Node(client, aeotec_radiator_thermostat_state) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="nortek_thermostat_added_event") def nortek_thermostat_added_event_fixture(client): """Mock a Nortek thermostat node added event.""" diff --git a/tests/components/zwave_js/test_number.py b/tests/components/zwave_js/test_number.py new file mode 100644 index 00000000000..b7d83068bea --- /dev/null +++ b/tests/components/zwave_js/test_number.py @@ -0,0 +1,69 @@ +"""Test the Z-Wave JS number platform.""" +from zwave_js_server.event import Event + +NUMBER_ENTITY = "number.thermostat_hvac_valve_control" + + +async def test_number(hass, client, aeotec_radiator_thermostat, integration): + """Test the number entity.""" + node = aeotec_radiator_thermostat + state = hass.states.get(NUMBER_ENTITY) + + assert state + assert state.state == "75.0" + + # Test turn on setting value + await hass.services.async_call( + "number", + "set_value", + {"entity_id": NUMBER_ENTITY, "value": 30}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 4 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "ccVersion": 1, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 30.0 + + client.async_send_command.reset_mock() + + # Test value update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 4, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "newValue": 99, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(NUMBER_ENTITY) + assert state.state == "99.0" diff --git a/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json b/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json new file mode 100644 index 00000000000..8cd6fe78201 --- /dev/null +++ b/tests/fixtures/zwave_js/aeotec_radiator_thermostat_state.json @@ -0,0 +1,626 @@ +{ + "nodeId": 4, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Thermostat", + "specific": "Thermostat General V2", + "mandatorySupportedCCs": [ + "Basic", + "Manufacturer Specific", + "Thermostat Mode", + "Thermostat Setpoint", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": false, + "isFrequentListening": true, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 881, + "productId": 21, + "productType": 2, + "firmwareVersion": "0.16", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 7, + "deviceConfig": { + "manufacturerId": 881, + "manufacturer": "Aeotec Ltd.", + "label": "Radiator Thermostat", + "description": "Thermostat - HVAC", + "devices": [{ "productType": "0x0002", "productId": "0x0015" }], + "firmwareVersion": { "min": "0.0", "max": "255.255" }, + "paramInformation": { "_map": {} } + }, + "label": "Radiator Thermostat", + "neighbors": [6, 7, 45, 67], + "interviewAttempts": 1, + "endpoints": [ + { "nodeId": 4, "index": 0, "installerIcon": 4608, "userIcon": 4608 } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 75 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { "switchType": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { "switchType": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "\u00b0C", + "label": "Air temperature", + "ccSpecific": { "sensorType": 1, "scale": 0 } + }, + "value": 19.37 + }, + { + "endpoint": 0, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 31, + "label": "Thermostat mode", + "states": { + "0": "Off", + "1": "Heat", + "11": "Energy heat", + "15": "Full power" + } + }, + "value": 31 + }, + { + "endpoint": 0, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 3, + "metadata": { "type": "any", "readable": true, "writeable": true } + }, + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 8, + "max": 28, + "unit": "\u00b0C", + "ccSpecific": { "setpointType": 1 } + }, + "value": 24 + }, + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyName": "setpoint", + "propertyKeyName": "Energy Save Heating", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 8, + "max": 28, + "unit": "\u00b0C", + "ccSpecific": { "setpointType": 11 } + }, + "value": 18 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Invert LCD orientation", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Normal orientation", + "1": "LCD content inverted" + }, + "label": "Invert LCD orientation", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "LCD Timeout", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 30, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "LCD Timeout", + "description": "LCD Timeout in seconds", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Backlight", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Backlight disabled", + "1": "Backlight enabled" + }, + "label": "Backlight", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Battery report", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Battery reporting disabled", + "1": "Battery reporting enabled" + }, + "label": "Battery report", + "description": "Battery reporting", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Measured Temperature", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 50, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Measured Temperature", + "description": "Measured Temperature report. Reporting Delta in 1/10 Celsius. '0' to disable reporting.", + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Valve position", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Valve position", + "description": "Valve position report. Reporting delta in percent. '0' to disable reporting.", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Window open detection", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 3, + "default": 2, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Detection Disabled", + "1": "Sensitivity low", + "2": "Sensitivity medium", + "3": "Sensitivity high" + }, + "label": "Window open detection", + "description": "Control 'Window open detection' sensitivity", + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Temperature Offset", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -128, + "max": 50, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Temperature Offset", + "description": "Measured Temperature offset", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyName": "Power Management", + "propertyKeyName": "Battery maintenance status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Battery maintenance status", + "states": { + "0": "idle", + "10": "Replace battery soon", + "11": "Replace battery now" + }, + "ccSpecific": { "notificationType": 8 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "System", + "propertyName": "System", + "propertyKeyName": "Hardware status", + "ccVersion": 8, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Hardware status", + "states": { + "0": "idle", + "3": "System hardware failure (with failure code)" + }, + "ccSpecific": { "notificationType": 9 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 881 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 21 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "local", + "propertyName": "local", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Local protection state", + "states": { + "0": "Unprotected", + "1": "ProtectedBySequence", + "2": "NoOperationPossible" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.61" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["0.16"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] + } \ No newline at end of file