diff --git a/homeassistant/components/zwave_mqtt/const.py b/homeassistant/components/zwave_mqtt/const.py index 4391d53ac4c..37270748a6a 100644 --- a/homeassistant/components/zwave_mqtt/const.py +++ b/homeassistant/components/zwave_mqtt/const.py @@ -1,9 +1,10 @@ """Constants for the zwave_mqtt integration.""" +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN DOMAIN = "zwave_mqtt" DATA_UNSUBSCRIBE = "unsubscribe" -PLATFORMS = [SWITCH_DOMAIN] +PLATFORMS = [SENSOR_DOMAIN, SWITCH_DOMAIN] # MQTT Topics TOPIC_OPENZWAVE = "OpenZWave" diff --git a/homeassistant/components/zwave_mqtt/discovery.py b/homeassistant/components/zwave_mqtt/discovery.py index e257e43b678..19bcd9818d2 100644 --- a/homeassistant/components/zwave_mqtt/discovery.py +++ b/homeassistant/components/zwave_mqtt/discovery.py @@ -5,6 +5,30 @@ from openzwavemqtt.const import CommandClass, ValueGenre, ValueType from . import const DISCOVERY_SCHEMAS = ( + { # All other text/numeric sensors + const.DISC_COMPONENT: "sensor", + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: ( + CommandClass.SENSOR_MULTILEVEL, + CommandClass.METER, + CommandClass.ALARM, + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + CommandClass.BATTERY, + CommandClass.NOTIFICATION, + CommandClass.BASIC, + ), + const.DISC_TYPE: ( + ValueType.DECIMAL, + ValueType.INT, + ValueType.STRING, + ValueType.BYTE, + ValueType.LIST, + ), + } + }, + }, { # Switch platform const.DISC_COMPONENT: "switch", const.DISC_GENERIC_DEVICE_CLASS: ( diff --git a/homeassistant/components/zwave_mqtt/sensor.py b/homeassistant/components/zwave_mqtt/sensor.py new file mode 100644 index 00000000000..309c2784405 --- /dev/null +++ b/homeassistant/components/zwave_mqtt/sensor.py @@ -0,0 +1,131 @@ +"""Representation of Z-Wave sensors.""" + +import logging + +from openzwavemqtt.const import CommandClass + +from homeassistant.components.sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DOMAIN as SENSOR_DOMAIN, +) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_UNSUBSCRIBE, DOMAIN +from .entity import ZWaveDeviceEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave sensor from config entry.""" + + @callback + def async_add_sensor(value): + """Add Z-Wave Sensor.""" + # Basic Sensor types + if isinstance(value.primary.value, (float, int)): + sensor = ZWaveNumericSensor(value) + + elif isinstance(value.primary.value, dict): + sensor = ZWaveListSensor(value) + + else: + _LOGGER.warning("Sensor not implemented for value %s", value.primary.label) + return + + async_add_entities([sensor]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, f"{DOMAIN}_new_{SENSOR_DOMAIN}", async_add_sensor + ) + ) + + +class ZwaveSensorBase(ZWaveDeviceEntity): + """Basic Representation of a Z-Wave sensor.""" + + @property + def device_class(self): + """Return the device class of the sensor.""" + if self.values.primary.command_class == CommandClass.BATTERY: + return DEVICE_CLASS_BATTERY + if self.values.primary.command_class == CommandClass.METER: + return DEVICE_CLASS_POWER + if "Temperature" in self.values.primary.label: + return DEVICE_CLASS_TEMPERATURE + if "Illuminance" in self.values.primary.label: + return DEVICE_CLASS_ILLUMINANCE + if "Humidity" in self.values.primary.label: + return DEVICE_CLASS_HUMIDITY + if "Power" in self.values.primary.label: + return DEVICE_CLASS_POWER + if "Energy" in self.values.primary.label: + return DEVICE_CLASS_POWER + if "Electric" in self.values.primary.label: + return DEVICE_CLASS_POWER + if "Pressure" in self.values.primary.label: + return DEVICE_CLASS_PRESSURE + return None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # We hide some of the more advanced sensors by default to not overwhelm users + if self.values.primary.command_class in [ + CommandClass.BASIC, + CommandClass.INDICATOR, + CommandClass.NOTIFICATION, + ]: + return False + return True + + +class ZWaveNumericSensor(ZwaveSensorBase): + """Representation of a Z-Wave sensor.""" + + @property + def state(self): + """Return state of the sensor.""" + return round(self.values.primary.value, 2) + + @property + def unit_of_measurement(self): + """Return unit of measurement the value is expressed in.""" + if self.values.primary.units == "C": + return TEMP_CELSIUS + if self.values.primary.units == "F": + return TEMP_FAHRENHEIT + + return self.values.primary.units + + +class ZWaveListSensor(ZwaveSensorBase): + """Representation of a Z-Wave list sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + # We use the id as value for backwards compatibility + return self.values.primary.value["Selected_id"] + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + attributes = super().device_state_attributes + # add the value's label as property + attributes["label"] = self.values.primary.value["Selected"] + return attributes + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # these sensors are only here for backwards compatibility, disable them by default + return False diff --git a/tests/components/zwave_mqtt/conftest.py b/tests/components/zwave_mqtt/conftest.py index 447348a4b6d..b32d6f72ce8 100644 --- a/tests/components/zwave_mqtt/conftest.py +++ b/tests/components/zwave_mqtt/conftest.py @@ -38,3 +38,14 @@ async def switch_msg_fixture(hass): message = MQTTMessage(topic=switch_json["topic"], payload=switch_json["payload"]) message.encode() return message + + +@pytest.fixture(name="sensor_msg") +async def sensor_msg_fixture(hass): + """Return a mock MQTT msg with a sensor change message.""" + sensor_json = json.loads( + await hass.async_add_executor_job(load_fixture, "zwave_mqtt/sensor.json") + ) + message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) + message.encode() + return message diff --git a/tests/components/zwave_mqtt/test_sensor.py b/tests/components/zwave_mqtt/test_sensor.py new file mode 100644 index 00000000000..ab7fd26b0b6 --- /dev/null +++ b/tests/components/zwave_mqtt/test_sensor.py @@ -0,0 +1,76 @@ +"""Test Z-Wave Sensors.""" +from homeassistant.components.sensor import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DOMAIN as SENSOR_DOMAIN, +) +from homeassistant.components.zwave_mqtt.const import DOMAIN +from homeassistant.const import ATTR_DEVICE_CLASS + +from .common import setup_zwave + + +async def test_sensor(hass, generic_data): + """Test setting up config entry.""" + await setup_zwave(hass, fixture=generic_data) + + # Test standard sensor + state = hass.states.get("sensor.smart_plug_electric_v") + assert state is not None + assert state.state == "123.9" + assert state.attributes["unit_of_measurement"] == "V" + + # Test device classes + state = hass.states.get("sensor.trisensor_relative_humidity") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_HUMIDITY + state = hass.states.get("sensor.trisensor_pressure") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_PRESSURE + state = hass.states.get("sensor.trisensor_fake_power") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + state = hass.states.get("sensor.trisensor_fake_energy") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + state = hass.states.get("sensor.trisensor_fake_electric") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + + # Test ZWaveListSensor disabled by default + registry = await hass.helpers.entity_registry.async_get_registry() + entity_id = "sensor.water_sensor_6_instance_1_water" + state = hass.states.get(entity_id) + assert state is None + + entry = registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" + + # Test enabling entity + updated_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entry + assert updated_entry.disabled is False + + +async def test_sensor_enabled(hass, generic_data, sensor_msg): + """Test enabling an advanced sensor.""" + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry = registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "1-36-1407375493578772", + suggested_object_id="water_sensor_6_instance_1_water", + disabled_by=None, + ) + assert entry.disabled is False + + receive_msg = await setup_zwave(hass, fixture=generic_data) + receive_msg(sensor_msg) + await hass.async_block_till_done() + + state = hass.states.get(entry.entity_id) + assert state is not None + assert state.state == "0" + assert state.attributes["label"] == "Clear" diff --git a/tests/fixtures/zwave_mqtt/generic_network_dump.csv b/tests/fixtures/zwave_mqtt/generic_network_dump.csv index fb2aa1dcfe1..debb329a8f7 100644 --- a/tests/fixtures/zwave_mqtt/generic_network_dump.csv +++ b/tests/fixtures/zwave_mqtt/generic_network_dump.csv @@ -157,7 +157,13 @@ OpenZWave/1/node/37/instance/1/commandclass/48/value/625737744/,{ "Label": "S OpenZWave/1/node/37/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1579566891} OpenZWave/1/node/37/instance/1/commandclass/49/value/281475602464786/,{ "Label": "Air Temperature", "Value": 20.700000762939454, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475602464786, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} OpenZWave/1/node/37/instance/1/commandclass/49/value/844425555886098/,{ "Label": "Illuminance", "Value": 0.0, "Units": "Lux", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "User", "Help": "Luminance Sensor Value", "ValueIDKey": 844425555886098, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/1234567890/,{ "Label": "Relative Humidity", "Value": 56.7, "Units": "%", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "User", "Help": "Humidity Sensor Value", "ValueIDKey": 1234567890, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678901/,{ "Label": "Pressure", "Value": 123, "Units": "inHg", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Pressure Sensor Value", "ValueIDKey": 12345678901, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} OpenZWave/1/node/37/instance/1/commandclass/49/value/72057594672070676/,{ "Label": "Air Temperature Units", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 37, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594672070676, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678902/,{ "Label": "Fake Power", "Value": 123, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 90, "Node": 37, "Genre": "User", "Help": "Power Sensor Value", "ValueIDKey": 12345678902, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678903/,{ "Label": "Fake Energy", "Value": 456, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 91, "Node": 37, "Genre": "User", "Help": "Energy Sensor Value", "ValueIDKey": 12345678903, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678904/,{ "Label": "Fake Electric", "Value": 789, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 92, "Node": 37, "Genre": "User", "Help": "Electric Sensor Value", "ValueIDKey": 12345678904, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678905/,{ "Label": "Fake String", "Value": "fake", "Units": "", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Fake String Sensor Value", "ValueIDKey": 12345678901, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} OpenZWave/1/node/37/instance/1/commandclass/49/value/72620544625491988/,{ "Label": "Illuminance Units", "Value": { "List": [ { "Value": 1, "Label": "Lux" } ], "Selected": "Lux" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 258, "Node": 37, "Genre": "System", "Help": "Luminance Sensor Available Units", "ValueIDKey": 72620544625491988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} OpenZWave/1/node/37/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} OpenZWave/1/node/37/instance/1/commandclass/94/value/634880017/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880017, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} diff --git a/tests/fixtures/zwave_mqtt/sensor.json b/tests/fixtures/zwave_mqtt/sensor.json new file mode 100644 index 00000000000..17b86f90809 --- /dev/null +++ b/tests/fixtures/zwave_mqtt/sensor.json @@ -0,0 +1,38 @@ +{ + "topic": "OpenZWave/1/node/36/instance/1/commandclass/113/value/1407375493578772/", + "payload": { + "Label": "Instance 1: Water", + "Value": { + "List": [ + { + "Value": 0, + "Label": "Clear" + }, + { + "Value": 2, + "Label": "Water Leak at Unknown Location" + } + ], + "Selected": "Clear", + "Selected_id": 0 + }, + "Units": "", + "Min": 0, + "Max": 0, + "Type": "List", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_NOTIFICATION", + "Index": 5, + "Node": 36, + "Genre": "User", + "Help": "Water Alerts", + "ValueIDKey": 1407375493578772, + "ReadOnly": false, + "WriteOnly": false, + "ValueSet": false, + "ValuePolled": false, + "ChangeVerified": false, + "Event": "valueAdded", + "TimeStamp": 1579566891 + } +} \ No newline at end of file