diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 9ec546be756299..afbe308e1b84d8 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -47,6 +47,7 @@ is_opening_state_notification_value, ) from .models import ( + FirmwareVersionRange, NewZWaveDiscoverySchema, ValueType, ZwaveDiscoveryInfo, @@ -1346,6 +1347,38 @@ def __init__( ), entity_class=ZWaveBooleanBinarySensor, ), + NewZWaveDiscoverySchema( + # Fibaro FGMS001 Motion Sensor: + # On firmware <= 2.8 the device supports Binary Sensor CC v1, which + # does not give us any information about the type of the sensor. + # As a result it is exposed via the generic "Any" sensor type, + # which fits no other discovery schema. + platform=Platform.BINARY_SENSOR, + manufacturer_id={0x010F}, + product_type={0x0800, 0x0801, 0x8800}, + product_id={ + 0x1001, + 0x1002, + 0x2001, + 0x2002, + 0x3001, + 0x3002, + 0x4001, + 0x4002, + 0x6001, + }, + firmware_version_range=FirmwareVersionRange(max="2.8"), + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SENSOR_BINARY}, + property={"Any"}, + type={ValueType.BOOLEAN}, + ), + entity_description=BinarySensorEntityDescription( + key="motion", + device_class=BinarySensorDeviceClass.MOTION, + ), + entity_class=ZWaveBooleanBinarySensor, + ), NewZWaveDiscoverySchema( platform=Platform.BINARY_SENSOR, primary_value=ZWaveValueDiscoverySchema( diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index d2b6cbeaeeb322..084c965e38ad6d 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -598,6 +598,17 @@ def hoppe_ehandle_connectsense_state_fixture() -> NodeDataType: ) +@pytest.fixture(name="fibaro_fgms001_v2_8_state") +def fibaro_fgms001_v2_8_state_fixture() -> NodeDataType: + """Load node state fixture data for Fibaro FGMS001 on firmware 2.8.""" + return cast( + NodeDataType, + # Note: this fixture was created from a simulated device. + # If necessary, replace it with one created from a real FGMS001 + load_json_object_fixture("fibaro_fgms001_v2_8_state.json", DOMAIN), + ) + + # model fixtures @@ -1492,3 +1503,13 @@ def hoppe_ehandle_connectsense_fixture( node = Node(client, hoppe_ehandle_connectsense_state) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="fibaro_fgms001_v2_8") +def fibaro_fgms001_v2_8_fixture( + client: MagicMock, fibaro_fgms001_v2_8_state: NodeDataType +) -> Node: + """Load node for Fibaro FGMS001 on firmware 2.8.""" + node = Node(client, fibaro_fgms001_v2_8_state) + client.driver.controller.nodes[node.node_id] = node + return node diff --git a/tests/components/zwave_js/fixtures/fibaro_fgms001_v2_8_state.json b/tests/components/zwave_js/fixtures/fibaro_fgms001_v2_8_state.json new file mode 100644 index 00000000000000..e2b98d2eb7d934 --- /dev/null +++ b/tests/components/zwave_js/fixtures/fibaro_fgms001_v2_8_state.json @@ -0,0 +1,198 @@ +{ + "nodeId": 2, + "index": 0, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 271, + "productId": 4097, + "productType": 2048, + "firmwareVersion": "2.8", + "deviceConfig": { + "filename": "/data/db/devices/0x010f/fgms001.json", + "manufacturerId": 271, + "manufacturer": "Fibargroup", + "label": "FGMS001", + "description": "Motion Sensor", + "devices": [ + { + "productType": 2048, + "productId": 4097 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "FGMS001", + "interviewAttempts": 1, + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 9600, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + } + }, + "interviewStage": "Complete", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x010f:0x0800:0x1001:2.8", + "highestSecurityClass": -1, + "isControllerNode": false, + "keepAwake": false, + "protocol": 0, + "values": [ + { + "endpoint": 0, + "commandClass": 48, + "commandClassName": "Binary Sensor", + "property": "Any", + "propertyName": "Any", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Sensor state (Any)", + "ccSpecific": { + "sensorType": 255 + }, + "stateful": true, + "secret": false + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 4097 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 2048 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535, + "stateful": true, + "secret": false + }, + "value": 271 + }, + { + "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 + }, + "value": ["2.8"] + } + ], + "endpoints": [ + { + "nodeId": 2, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing End Node" + }, + "generic": { + "key": 7, + "label": "Notification Sensor" + }, + "specific": { + "key": 1, + "label": "Notification Sensor" + } + }, + "commandClasses": [ + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 48, + "name": "Binary Sensor", + "version": 1, + "isSecure": false + } + ] + } + ] +} diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index 97f94341b6470c..f5ec08d569401e 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -7,6 +7,10 @@ from zwave_js_server.event import Event from zwave_js_server.model.node import Node +from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDeviceClass, +) from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode from homeassistant.components.number import ( @@ -28,8 +32,10 @@ from homeassistant.components.zwave_js.discovery_data_template import ( DynamicCurrentTempClimateDataTemplate, ) +from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_OFF, STATE_UNKNOWN, @@ -658,3 +664,47 @@ async def test_nabu_casa_zwa2_legacy( assert state.attributes["friendly_name"] == "Home Assistant Connect ZWA-2 LED", ( "The LED should have the correct friendly name" ) + + +@pytest.mark.parametrize("platforms", [[Platform.BINARY_SENSOR, Platform.LIGHT]]) +async def test_fibaro_fgms001_v2_8_motion_discovery( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + device_registry: dr.DeviceRegistry, + client: MagicMock, + fibaro_fgms001_v2_8: Node, + integration: MockConfigEntry, +) -> None: + """Test the Fibaro FGMS001 on firmware 2.8 is discovered as a motion sensor. + + The device exposes its motion state via the Sensor Binary CC under the + "Any" sensor type. Without the device-specific discovery override the + value would either fall through to the disabled legacy boolean schema + or be misclassified, so we assert that exactly one binary_sensor entity + with device_class=motion is created and no light entity exists. + """ + device = device_registry.async_get_device( + identifiers={get_device_id(client.driver, fibaro_fgms001_v2_8)} + ) + assert device is not None + + entries = er.async_entries_for_device( + entity_registry, device.id, include_disabled_entities=True + ) + + motion_entries = [ + entry + for entry in entries + if entry.domain == BINARY_SENSOR_DOMAIN + and entry.original_device_class == BinarySensorDeviceClass.MOTION + ] + assert len(motion_entries) == 1 + motion_entry = motion_entries[0] + assert motion_entry.disabled_by is None + + state = hass.states.get(motion_entry.entity_id) + assert state is not None + assert state.state == STATE_OFF + assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.MOTION + + assert not [entry for entry in entries if entry.domain == Platform.LIGHT]