diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index ab8e40edf09..6dc5cc58df5 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -252,6 +252,28 @@ NOTIFICATION_SENSOR_MAPPINGS: List[NotificationSensorMapping] = [ }, ] +PROPERTY_DOOR_STATUS = "doorStatus" + + +class PropertySensorMapping(TypedDict, total=False): + """Represent a property sensor mapping dict type.""" + + property_name: str # required + on_states: List[str] # required + device_class: str + enabled: bool + + +# Mappings for property sensors +PROPERTY_SENSOR_MAPPINGS: List[PropertySensorMapping] = [ + { + "property_name": PROPERTY_DOOR_STATUS, + "on_states": ["open"], + "device_class": DEVICE_CLASS_DOOR, + "enabled": True, + }, +] + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable @@ -266,6 +288,8 @@ async def async_setup_entry( if info.platform_hint == "notification": entities.append(ZWaveNotificationBinarySensor(config_entry, client, info)) + elif info.platform_hint == "property": + entities.append(ZWavePropertyBinarySensor(config_entry, client, info)) else: # boolean sensor entities.append(ZWaveBooleanBinarySensor(config_entry, client, info)) @@ -356,3 +380,43 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): mapping_info = mapping.copy() return mapping_info return {} + + +class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): + """Representation of a Z-Wave binary_sensor from a property.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZWavePropertyBinarySensor entity.""" + super().__init__(config_entry, client, info) + # check if we have a custom mapping for this value + self._mapping_info = self._get_sensor_mapping() + + @property + def is_on(self) -> bool: + """Return if the sensor is on or off.""" + return self.info.primary_value.value in self._mapping_info["on_states"] + + @property + def device_class(self) -> Optional[str]: + """Return device class.""" + return self._mapping_info.get("device_class") + + @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 more advanced sensors by default to not overwhelm users + # unless explicitly stated in a mapping, assume deisabled by default + return self._mapping_info.get("enabled", False) + + @callback + def _get_sensor_mapping(self) -> PropertySensorMapping: + """Try to get a device specific mapping for this sensor.""" + mapping_info = PropertySensorMapping() + for mapping in PROPERTY_SENSOR_MAPPINGS: + if mapping["property_name"] == self.info.primary_value.property_name: + mapping_info = mapping.copy() + break + + return mapping_info diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 7bef6eb603f..12ffcc576b7 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -74,6 +74,24 @@ DISCOVERY_SCHEMAS = [ property={"currentMode", "locked"}, type={"number", "boolean"}, ), + # door lock door status + ZWaveDiscoverySchema( + platform="binary_sensor", + hint="property", + device_class_generic={"Entry Control"}, + device_class_specific={ + "Door Lock", + "Advanced Door Lock", + "Secure Keypad Door Lock", + "Secure Lockbox", + }, + command_class={ + CommandClass.LOCK, + CommandClass.DOOR_LOCK, + }, + property={"doorStatus"}, + type={"any"}, + ), # climate ZWaveDiscoverySchema( platform="climate", diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py index 0b9d7e1e89f..a7be137657c 100644 --- a/tests/components/zwave_js/common.py +++ b/tests/components/zwave_js/common.py @@ -7,3 +7,6 @@ LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level" ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any" DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any" NOTIFICATION_MOTION_BINARY_SENSOR = "binary_sensor.multisensor_6_motion_sensor_status" +PROPERTY_DOOR_STATUS_BINARY_SENSOR = ( + "binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door" +) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 6bf4afefd62..be0390ab047 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -68,6 +68,12 @@ def lock_schlage_be469_state_fixture(): return json.loads(load_fixture("zwave_js/lock_schlage_be469_state.json")) +@pytest.fixture(name="lock_august_asl03_state", scope="session") +def lock_august_asl03_state_fixture(): + """Load the August Pro lock node state fixture data.""" + return json.loads(load_fixture("zwave_js/lock_august_asl03_state.json")) + + @pytest.fixture(name="climate_radio_thermostat_ct100_plus_state", scope="session") def climate_radio_thermostat_ct100_plus_state_fixture(): """Load the climate radio thermostat ct100 plus node state fixture data.""" @@ -170,6 +176,14 @@ def lock_schlage_be469_fixture(client, lock_schlage_be469_state): return node +@pytest.fixture(name="lock_august_pro") +def lock_august_asl03_fixture(client, lock_august_asl03_state): + """Mock a August Pro lock node.""" + node = Node(client, lock_august_asl03_state) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="climate_radio_thermostat_ct100_plus") def climate_radio_thermostat_ct100_plus_fixture( client, climate_radio_thermostat_ct100_plus_state diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py index e92071c48f9..e8361d8b03e 100644 --- a/tests/components/zwave_js/test_binary_sensor.py +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -9,6 +9,7 @@ from .common import ( ENABLED_LEGACY_BINARY_SENSOR, LOW_BATTERY_BINARY_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR, + PROPERTY_DOOR_STATUS_BINARY_SENSOR, ) @@ -84,3 +85,56 @@ async def test_notification_sensor(hass, multisensor_6, integration): assert state assert state.state == STATE_ON assert state.attributes["device_class"] == DEVICE_CLASS_MOTION + + +async def test_property_sensor_door_status(hass, lock_august_pro, integration): + """Test property binary sensor with sensor mapping (doorStatus).""" + node = lock_august_pro + + state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) + assert state is not None + assert state.state == STATE_OFF + + # open door + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 6, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "doorStatus", + "newValue": "open", + "prevValue": "closed", + "propertyName": "doorStatus", + }, + }, + ) + node.receive_event(event) + state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) + assert state.state == STATE_ON + + # close door + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 6, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "doorStatus", + "newValue": "closed", + "prevValue": "open", + "propertyName": "doorStatus", + }, + }, + ) + node.receive_event(event) + state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) + assert state.state == STATE_OFF diff --git a/tests/fixtures/zwave_js/lock_august_asl03_state.json b/tests/fixtures/zwave_js/lock_august_asl03_state.json new file mode 100644 index 00000000000..b6d44341853 --- /dev/null +++ b/tests/fixtures/zwave_js/lock_august_asl03_state.json @@ -0,0 +1,456 @@ +{ + "nodeId": 6, + "index": 0, + "installerIcon": 768, + "userIcon": 768, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Entry Control", + "specific": "Secure Keypad Door Lock", + "mandatorySupportedCCs": [ + "Basic", + "Door Lock", + "User Code", + "Manufacturer Specific", + "Security", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": false, + "isFrequentListening": true, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": true, + "version": 4, + "isBeaming": true, + "manufacturerId": 831, + "productId": 1, + "productType": 1, + "firmwareVersion": "1.59", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 7, + "deviceConfig": { + "manufacturerId": 831, + "manufacturer": "August Smart Locks", + "label": "ASL-03", + "description": "August Smart Lock Pro 3rd Gen", + "devices": [ + { + "productType": "0x0000", + "productId": "0x0594" + }, + { + "productType": "0x0000", + "productId": "0xdf29" + }, + { + "productType": "0x0001", + "productId": "0x0001" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + } + }, + "label": "ASL-03", + "neighbors": [ + 1, + 7, + 8, + 9 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 6, + "index": 0, + "installerIcon": 768, + "userIcon": 768 + } + ], + "values": [ + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "currentMode", + "propertyName": "currentMode", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current lock mode", + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 255 + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "targetMode", + "propertyName": "targetMode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target lock mode", + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + } + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "outsideHandlesCanOpenDoor", + "propertyName": "outsideHandlesCanOpenDoor", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which outside handles can open the door (actual status)" + }, + "value": [ + false, + false, + false, + false + ] + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "insideHandlesCanOpenDoor", + "propertyName": "insideHandlesCanOpenDoor", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which inside handles can open the door (actual status)" + }, + "value": [ + true, + false, + false, + false + ] + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "latchStatus", + "propertyName": "latchStatus", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the latch" + }, + "value": "open" + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "boltStatus", + "propertyName": "boltStatus", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the bolt" + }, + "value": "locked" + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "doorStatus", + "propertyName": "doorStatus", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the door" + }, + "value": "closed" + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "lockTimeout", + "propertyName": "lockTimeout", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Seconds until lock mode times out" + } + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "operationType", + "propertyName": "operationType", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Lock operation type", + "states": { + "1": "Constant", + "2": "Timed" + } + }, + "value": 1 + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "outsideHandlesCanOpenDoorConfiguration", + "propertyName": "outsideHandlesCanOpenDoorConfiguration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which outside handles can open the door (configuration)" + }, + "value": [ + false, + false, + false, + false + ] + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "insideHandlesCanOpenDoorConfiguration", + "propertyName": "insideHandlesCanOpenDoorConfiguration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which inside handles can open the door (configuration)" + }, + "value": [ + true, + false, + false, + false + ] + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "lockTimeoutConfiguration", + "propertyName": "lockTimeoutConfiguration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 65535, + "label": "Duration of timed mode in seconds" + } + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Access Control", + "propertyKey": "Lock state", + "propertyName": "Access Control", + "propertyKeyName": "Lock state", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Lock state", + "states": { + "0": "idle", + "11": "Lock jammed" + }, + "ccSpecific": { + "notificationType": 6 + } + }, + "value": 0 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 831 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 1 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 1 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.61" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.59" + ] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} \ No newline at end of file