From 2b2d7558dec25dd21d66626fc9da73e39d359cf3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 8 Nov 2020 03:42:20 +0100 Subject: [PATCH] Add binary_sensor platform to onewire integration (#42832) * Add binary_sensor platform to onewire integration * Keep the same name * Rework tests * Rework tests --- .../components/onewire/binary_sensor.py | 132 ++++++++++++++ homeassistant/components/onewire/const.py | 4 + .../components/onewire/onewire_entities.py | 10 +- tests/components/onewire/__init__.py | 27 +++ .../components/onewire/test_binary_sensor.py | 86 +++++++++ .../onewire/test_entity_owserver.py | 163 ++++++++++++++---- .../components/onewire/test_entity_sysbus.py | 1 + 7 files changed, 388 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/onewire/binary_sensor.py create mode 100644 tests/components/onewire/test_binary_sensor.py diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py new file mode 100644 index 00000000000..a6c4721de54 --- /dev/null +++ b/homeassistant/components/onewire/binary_sensor.py @@ -0,0 +1,132 @@ +"""Support for 1-Wire binary sensors.""" +import os + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.const import CONF_TYPE + +from .const import CONF_TYPE_OWSERVER, DOMAIN, SENSOR_TYPE_SENSED +from .onewire_entities import OneWireProxy +from .onewirehub import OneWireHub + +DEVICE_BINARY_SENSORS = { + # Family : { path, sensor_type } + "12": [ + { + "path": "sensed.A", + "name": "Sensed A", + "type": SENSOR_TYPE_SENSED, + "default_disabled": True, + }, + { + "path": "sensed.B", + "name": "Sensed B", + "type": SENSOR_TYPE_SENSED, + "default_disabled": True, + }, + ], + "29": [ + { + "path": "sensed.0", + "name": "Sensed 0", + "type": SENSOR_TYPE_SENSED, + "default_disabled": True, + }, + { + "path": "sensed.1", + "name": "Sensed 1", + "type": SENSOR_TYPE_SENSED, + "default_disabled": True, + }, + { + "path": "sensed.2", + "name": "Sensed 2", + "type": SENSOR_TYPE_SENSED, + "default_disabled": True, + }, + { + "path": "sensed.3", + "name": "Sensed 3", + "type": SENSOR_TYPE_SENSED, + "default_disabled": True, + }, + { + "path": "sensed.4", + "name": "Sensed 4", + "type": SENSOR_TYPE_SENSED, + "default_disabled": True, + }, + { + "path": "sensed.5", + "name": "Sensed 5", + "type": SENSOR_TYPE_SENSED, + "default_disabled": True, + }, + { + "path": "sensed.6", + "name": "Sensed 6", + "type": SENSOR_TYPE_SENSED, + "default_disabled": True, + }, + { + "path": "sensed.7", + "name": "Sensed 7", + "type": SENSOR_TYPE_SENSED, + "default_disabled": True, + }, + ], +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up 1-Wire platform.""" + # Only OWServer implementation works with binary sensors + if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER: + onewirehub = hass.data[DOMAIN][config_entry.unique_id] + + entities = await hass.async_add_executor_job(get_entities, onewirehub) + async_add_entities(entities, True) + + +def get_entities(onewirehub: OneWireHub): + """Get a list of entities.""" + entities = [] + + for device in onewirehub.devices: + family = device["family"] + device_type = device["type"] + sensor_id = os.path.split(os.path.split(device["path"])[0])[1] + + if family not in DEVICE_BINARY_SENSORS: + continue + device_info = { + "identifiers": {(DOMAIN, sensor_id)}, + "manufacturer": "Maxim Integrated", + "model": device_type, + "name": sensor_id, + } + for device_sensor in DEVICE_BINARY_SENSORS[family]: + device_file = os.path.join( + os.path.split(device["path"])[0], device_sensor["path"] + ) + entities.append( + OneWireBinarySensor( + sensor_id, + device_file, + device_sensor["type"], + device_sensor["name"], + device_info, + device_sensor.get("default_disabled", False), + onewirehub.owproxy, + ) + ) + + return entities + + +class OneWireBinarySensor(BinarySensorEntity, OneWireProxy): + """Implementation of a 1-Wire binary sensor.""" + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index 7666386e4b3..a129b3600cd 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -1,4 +1,5 @@ """Constants for 1-Wire component.""" +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( DEVICE_CLASS_CURRENT, @@ -36,6 +37,7 @@ SENSOR_TYPE_HUMIDITY = "humidity" SENSOR_TYPE_ILLUMINANCE = "illuminance" SENSOR_TYPE_MOISTURE = "moisture" SENSOR_TYPE_PRESSURE = "pressure" +SENSOR_TYPE_SENSED = "sensed" SENSOR_TYPE_TEMPERATURE = "temperature" SENSOR_TYPE_VOLTAGE = "voltage" SENSOR_TYPE_WETNESS = "wetness" @@ -51,8 +53,10 @@ SENSOR_TYPES = { SENSOR_TYPE_COUNT: ["count", None], SENSOR_TYPE_VOLTAGE: [VOLT, DEVICE_CLASS_VOLTAGE], SENSOR_TYPE_CURRENT: [ELECTRICAL_CURRENT_AMPERE, DEVICE_CLASS_CURRENT], + SENSOR_TYPE_SENSED: [None, None], } SUPPORTED_PLATFORMS = [ + BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN, ] diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index 6f727cd2506..7196ae7af99 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -7,7 +7,7 @@ from pyownet import protocol from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import StateType -from .const import SENSOR_TYPES +from .const import SENSOR_TYPE_COUNT, SENSOR_TYPE_SENSED, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -86,12 +86,12 @@ class OneWireProxy(OneWire): sensor_type: str, sensor_name: str, device_info: Dict[str, Any], - disable_startup: bool, + default_disabled: bool, owproxy: protocol._Proxy, ): """Initialize the sensor.""" super().__init__( - name, device_file, sensor_type, sensor_name, device_info, disable_startup + name, device_file, sensor_type, sensor_name, device_info, default_disabled ) self._owproxy = owproxy @@ -107,8 +107,10 @@ class OneWireProxy(OneWire): except protocol.Error as exc: _LOGGER.error("Owserver failure in read(), got: %s", exc) else: - if "count" in self._unit_of_measurement: + if self._sensor_type == SENSOR_TYPE_COUNT: value = int(self._value_raw) + elif self._sensor_type == SENSOR_TYPE_SENSED: + value = int(self._value_raw) == 1 else: value = round(self._value_raw, 1) diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 561e5a92a23..eb9b42ea996 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -2,6 +2,7 @@ from homeassistant.components.onewire.const import ( CONF_MOUNT_DIR, + CONF_NAMES, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS, DEFAULT_SYSBUS_MOUNT_DIR, @@ -63,3 +64,29 @@ async def setup_onewire_owserver_integration(hass): await hass.async_block_till_done() return config_entry + + +async def setup_onewire_patched_owserver_integration(hass): + """Create the 1-Wire integration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source="user", + data={ + CONF_TYPE: CONF_TYPE_OWSERVER, + CONF_HOST: "1.2.3.4", + CONF_PORT: "1234", + CONF_NAMES: { + "10.111111111111": "My DS18B20", + }, + }, + unique_id=f"{CONF_TYPE_OWSERVER}:1.2.3.4:1234", + connection_class=CONN_CLASS_LOCAL_POLL, + options={}, + entry_id="2", + ) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py new file mode 100644 index 00000000000..bc8cf1defc0 --- /dev/null +++ b/tests/components/onewire/test_binary_sensor.py @@ -0,0 +1,86 @@ +"""Tests for 1-Wire devices connected on OWServer.""" +import copy + +from pyownet.protocol import Error as ProtocolError +import pytest + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.onewire.binary_sensor import DEVICE_BINARY_SENSORS +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + +from . import setup_onewire_patched_owserver_integration + +from tests.async_mock import patch +from tests.common import mock_registry + +MOCK_DEVICE_SENSORS = { + "12.111111111111": { + "inject_reads": [ + b"DS2406", # read device type + ], + BINARY_SENSOR_DOMAIN: [ + { + "entity_id": "binary_sensor.12_111111111111_sensed_a", + "injected_value": b" 1", + "result": STATE_ON, + }, + { + "entity_id": "binary_sensor.12_111111111111_sensed_b", + "injected_value": b" 0", + "result": STATE_OFF, + }, + ], + }, +} + + +@pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys()) +@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") +async def test_owserver_binary_sensor(owproxy, hass, device_id): + """Test for 1-Wire binary sensor. + + This test forces all entities to be enabled. + """ + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + + mock_device_sensor = MOCK_DEVICE_SENSORS[device_id] + + device_family = device_id[0:2] + dir_return_value = [f"/{device_id}/"] + read_side_effect = [device_family.encode()] + if "inject_reads" in mock_device_sensor: + read_side_effect += mock_device_sensor["inject_reads"] + + expected_sensors = mock_device_sensor[BINARY_SENSOR_DOMAIN] + for expected_sensor in expected_sensors: + read_side_effect.append(expected_sensor["injected_value"]) + + # Ensure enough read side effect + read_side_effect.extend([ProtocolError("Missing injected value")] * 10) + owproxy.return_value.dir.return_value = dir_return_value + owproxy.return_value.read.side_effect = read_side_effect + + # Force enable binary sensors + patch_device_binary_sensors = copy.deepcopy(DEVICE_BINARY_SENSORS) + for item in patch_device_binary_sensors[device_family]: + item["default_disabled"] = False + + with patch( + "homeassistant.components.onewire.SUPPORTED_PLATFORMS", [BINARY_SENSOR_DOMAIN] + ), patch.dict( + "homeassistant.components.onewire.binary_sensor.DEVICE_BINARY_SENSORS", + patch_device_binary_sensors, + ): + await setup_onewire_patched_owserver_integration(hass) + await hass.async_block_till_done() + + assert len(entity_registry.entities) == len(expected_sensors) + + for expected_sensor in expected_sensors: + entity_id = expected_sensor["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + state = hass.states.get(entity_id) + assert state.state == expected_sensor["result"] diff --git a/tests/components/onewire/test_entity_owserver.py b/tests/components/onewire/test_entity_owserver.py index 247721e6c76..3afad03849e 100644 --- a/tests/components/onewire/test_entity_owserver.py +++ b/tests/components/onewire/test_entity_owserver.py @@ -2,10 +2,11 @@ from pyownet.protocol import Error as ProtocolError import pytest +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.onewire.const import ( - DEFAULT_OWSERVER_PORT, DOMAIN, PRESSURE_CBAR, + SUPPORTED_PLATFORMS, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( @@ -19,31 +20,24 @@ from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, PRESSURE_MBAR, + STATE_OFF, + STATE_ON, TEMP_CELSIUS, VOLT, ) from homeassistant.setup import async_setup_component +from . import setup_onewire_patched_owserver_integration + from tests.async_mock import patch from tests.common import mock_device_registry, mock_registry -MOCK_CONFIG = { - SENSOR_DOMAIN: { - "platform": DOMAIN, - "host": "localhost", - "port": DEFAULT_OWSERVER_PORT, - "names": { - "10.111111111111": "My DS18B20", - }, - } -} - MOCK_DEVICE_SENSORS = { "00.111111111111": { "inject_reads": [ b"", # read device type ], - "sensors": [], + SENSOR_DOMAIN: [], }, "10.111111111111": { "inject_reads": [ @@ -55,7 +49,7 @@ MOCK_DEVICE_SENSORS = { "model": "DS18S20", "name": "10.111111111111", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.my_ds18b20_temperature", "unique_id": "/10.111111111111/temperature", @@ -76,7 +70,27 @@ MOCK_DEVICE_SENSORS = { "model": "DS2406", "name": "12.111111111111", }, - "sensors": [ + BINARY_SENSOR_DOMAIN: [ + { + "entity_id": "binary_sensor.12_111111111111_sensed_a", + "unique_id": "/12.111111111111/sensed.A", + "injected_value": b" 1", + "result": STATE_ON, + "unit": None, + "class": None, + "disabled": True, + }, + { + "entity_id": "binary_sensor.12_111111111111_sensed_b", + "unique_id": "/12.111111111111/sensed.B", + "injected_value": b" 0", + "result": STATE_OFF, + "unit": None, + "class": None, + "disabled": True, + }, + ], + SENSOR_DOMAIN: [ { "entity_id": "sensor.12_111111111111_temperature", "unique_id": "/12.111111111111/TAI8570/temperature", @@ -107,7 +121,7 @@ MOCK_DEVICE_SENSORS = { "model": "DS2423", "name": "1D.111111111111", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.1d_111111111111_counter_a", "unique_id": "/1D.111111111111/counter.A", @@ -136,7 +150,7 @@ MOCK_DEVICE_SENSORS = { "model": "DS1822", "name": "22.111111111111", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.22_111111111111_temperature", "unique_id": "/22.111111111111/temperature", @@ -157,7 +171,7 @@ MOCK_DEVICE_SENSORS = { "model": "DS2438", "name": "26.111111111111", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.26_111111111111_temperature", "unique_id": "/26.111111111111/temperature", @@ -268,7 +282,7 @@ MOCK_DEVICE_SENSORS = { "model": "DS18B20", "name": "28.111111111111", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.28_111111111111_temperature", "unique_id": "/28.111111111111/temperature", @@ -279,6 +293,91 @@ MOCK_DEVICE_SENSORS = { }, ], }, + "29.111111111111": { + "inject_reads": [ + b"DS2408", # read device type + ], + "device_info": { + "identifiers": {(DOMAIN, "29.111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "DS2408", + "name": "29.111111111111", + }, + BINARY_SENSOR_DOMAIN: [ + { + "entity_id": "binary_sensor.29_111111111111_sensed_0", + "unique_id": "/29.111111111111/sensed.0", + "injected_value": b" 1", + "result": STATE_ON, + "unit": None, + "class": None, + "disabled": True, + }, + { + "entity_id": "binary_sensor.29_111111111111_sensed_1", + "unique_id": "/29.111111111111/sensed.1", + "injected_value": b" 0", + "result": STATE_OFF, + "unit": None, + "class": None, + "disabled": True, + }, + { + "entity_id": "binary_sensor.29_111111111111_sensed_2", + "unique_id": "/29.111111111111/sensed.2", + "injected_value": b" 0", + "result": STATE_OFF, + "unit": None, + "class": None, + "disabled": True, + }, + { + "entity_id": "binary_sensor.29_111111111111_sensed_3", + "unique_id": "/29.111111111111/sensed.3", + "injected_value": b" 0", + "result": STATE_OFF, + "unit": None, + "class": None, + "disabled": True, + }, + { + "entity_id": "binary_sensor.29_111111111111_sensed_4", + "unique_id": "/29.111111111111/sensed.4", + "injected_value": b" 0", + "result": STATE_OFF, + "unit": None, + "class": None, + "disabled": True, + }, + { + "entity_id": "binary_sensor.29_111111111111_sensed_5", + "unique_id": "/29.111111111111/sensed.5", + "injected_value": b" 0", + "result": STATE_OFF, + "unit": None, + "class": None, + "disabled": True, + }, + { + "entity_id": "binary_sensor.29_111111111111_sensed_6", + "unique_id": "/29.111111111111/sensed.6", + "injected_value": b" 0", + "result": STATE_OFF, + "unit": None, + "class": None, + "disabled": True, + }, + { + "entity_id": "binary_sensor.29_111111111111_sensed_7", + "unique_id": "/29.111111111111/sensed.7", + "injected_value": b" 0", + "result": STATE_OFF, + "unit": None, + "class": None, + "disabled": True, + }, + ], + }, "3B.111111111111": { "inject_reads": [ b"DS1825", # read device type @@ -289,7 +388,7 @@ MOCK_DEVICE_SENSORS = { "model": "DS1825", "name": "3B.111111111111", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.3b_111111111111_temperature", "unique_id": "/3B.111111111111/temperature", @@ -310,7 +409,7 @@ MOCK_DEVICE_SENSORS = { "model": "DS28EA00", "name": "42.111111111111", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.42_111111111111_temperature", "unique_id": "/42.111111111111/temperature", @@ -331,7 +430,7 @@ MOCK_DEVICE_SENSORS = { "model": "HobbyBoards_EF", "name": "EF.111111111111", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.ef_111111111111_humidity", "unique_id": "/EF.111111111111/humidity/humidity_corrected", @@ -372,7 +471,7 @@ MOCK_DEVICE_SENSORS = { "model": "HB_MOISTURE_METER", "name": "EF.111111111112", }, - "sensors": [ + SENSOR_DOMAIN: [ { "entity_id": "sensor.ef_111111111112_wetness_0", "unique_id": "/EF.111111111112/moisture/sensor.0", @@ -411,7 +510,9 @@ MOCK_DEVICE_SENSORS = { @pytest.mark.parametrize("device_id", MOCK_DEVICE_SENSORS.keys()) -async def test_owserver_setup_valid_device(hass, device_id): +@pytest.mark.parametrize("platform", SUPPORTED_PLATFORMS) +@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") +async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): """Test for 1-Wire device. As they would be on a clean setup: all binary-sensors and switches disabled. @@ -422,23 +523,23 @@ async def test_owserver_setup_valid_device(hass, device_id): mock_device_sensor = MOCK_DEVICE_SENSORS[device_id] + device_family = device_id[0:2] dir_return_value = [f"/{device_id}/"] - read_side_effect = [device_id[0:2].encode()] + read_side_effect = [device_family.encode()] if "inject_reads" in mock_device_sensor: read_side_effect += mock_device_sensor["inject_reads"] - expected_sensors = mock_device_sensor["sensors"] + expected_sensors = mock_device_sensor.get(platform, []) for expected_sensor in expected_sensors: read_side_effect.append(expected_sensor["injected_value"]) # Ensure enough read side effect read_side_effect.extend([ProtocolError("Missing injected value")] * 10) + owproxy.return_value.dir.return_value = dir_return_value + owproxy.return_value.read.side_effect = read_side_effect - with patch("homeassistant.components.onewire.onewirehub.protocol.proxy") as owproxy: - owproxy.return_value.dir.return_value = dir_return_value - owproxy.return_value.read.side_effect = read_side_effect - - assert await async_setup_component(hass, SENSOR_DOMAIN, MOCK_CONFIG) + with patch("homeassistant.components.onewire.SUPPORTED_PLATFORMS", [platform]): + await setup_onewire_patched_owserver_integration(hass) await hass.async_block_till_done() assert len(entity_registry.entities) == len(expected_sensors) diff --git a/tests/components/onewire/test_entity_sysbus.py b/tests/components/onewire/test_entity_sysbus.py index 242939d3a37..3ec46c60837 100644 --- a/tests/components/onewire/test_entity_sysbus.py +++ b/tests/components/onewire/test_entity_sysbus.py @@ -79,6 +79,7 @@ MOCK_DEVICE_SENSORS = { }, ], }, + "29-111111111111": {"sensors": []}, "3B-111111111111": { "device_info": { "identifiers": {(DOMAIN, "3B-111111111111")},