diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 17c772121d0..038a06fe8d2 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -16,6 +16,7 @@ _LOGGER = logging.getLogger(__name__) _PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/onewire/select.py b/homeassistant/components/onewire/select.py new file mode 100644 index 00000000000..e2ee3b9222c --- /dev/null +++ b/homeassistant/components/onewire/select.py @@ -0,0 +1,95 @@ +"""Support for 1-Wire environment select entities.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +import os + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import READ_MODE_INT +from .entity import OneWireEntity, OneWireEntityDescription +from .onewirehub import OneWireConfigEntry, OneWireHub + +# the library uses non-persistent connections +# and concurrent access to the bus is managed by the server +PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(seconds=30) + + +@dataclass(frozen=True) +class OneWireSelectEntityDescription(OneWireEntityDescription, SelectEntityDescription): + """Class describing OneWire select entities.""" + + +ENTITY_DESCRIPTIONS: dict[str, tuple[OneWireEntityDescription, ...]] = { + "28": ( + OneWireSelectEntityDescription( + key="tempres", + entity_category=EntityCategory.CONFIG, + read_mode=READ_MODE_INT, + options=["9", "10", "11", "12"], + translation_key="tempres", + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: OneWireConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up 1-Wire platform.""" + entities = await hass.async_add_executor_job( + get_entities, config_entry.runtime_data + ) + async_add_entities(entities, True) + + +def get_entities(onewire_hub: OneWireHub) -> list[OneWireSelectEntity]: + """Get a list of entities.""" + if not onewire_hub.devices: + return [] + + entities: list[OneWireSelectEntity] = [] + + for device in onewire_hub.devices: + family = device.family + device_id = device.id + device_info = device.device_info + + if family not in ENTITY_DESCRIPTIONS: + continue + for description in ENTITY_DESCRIPTIONS[family]: + device_file = os.path.join(os.path.split(device.path)[0], description.key) + entities.append( + OneWireSelectEntity( + description=description, + device_id=device_id, + device_file=device_file, + device_info=device_info, + owproxy=onewire_hub.owproxy, + ) + ) + + return entities + + +class OneWireSelectEntity(OneWireEntity, SelectEntity): + """Implementation of a 1-Wire switch.""" + + entity_description: OneWireSelectEntityDescription + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + return str(self._state) + + def select_option(self, option: str) -> None: + """Change the selected option.""" + self._write_value(option.encode("ascii")) diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json index cd8615dc5aa..9613a927f8d 100644 --- a/homeassistant/components/onewire/strings.json +++ b/homeassistant/components/onewire/strings.json @@ -41,6 +41,17 @@ "name": "Hub short on branch {id}" } }, + "select": { + "tempres": { + "name": "Temperature resolution", + "state": { + "9": "9 bits (0.5°C, fastest, up to 93.75ms)", + "10": "10 bits (0.25°C, up to 187.5ms)", + "11": "11 bits (0.125°C, up to 375ms)", + "12": "12 bits (0.0625°C, slowest, up to 750ms)" + } + } + }, "sensor": { "counter_id": { "name": "Counter {id}" diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 0ce725d1a0a..4c05442eadc 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -92,6 +92,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_INJECT_READS: { "/type": [b"DS18B20"], "/temperature": [b" 26.984"], + "/tempres": [b" 12"], }, }, "28.222222222222": { diff --git a/tests/components/onewire/snapshots/test_select.ambr b/tests/components/onewire/snapshots/test_select.ambr new file mode 100644 index 00000000000..7c4027cd046 --- /dev/null +++ b/tests/components/onewire/snapshots/test_select.ambr @@ -0,0 +1,62 @@ +# serializer version: 1 +# name: test_selects[select.28_111111111111_temperature_resolution-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '9', + '10', + '11', + '12', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.28_111111111111_temperature_resolution', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Temperature resolution', + 'platform': 'onewire', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tempres', + 'unique_id': '/28.111111111111/tempres', + 'unit_of_measurement': None, + }) +# --- +# name: test_selects[select.28_111111111111_temperature_resolution-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_file': '/28.111111111111/tempres', + 'friendly_name': '28.111111111111 Temperature resolution', + 'options': list([ + '9', + '10', + '11', + '12', + ]), + 'raw_value': 12.0, + }), + 'context': , + 'entity_id': 'select.28_111111111111_temperature_resolution', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- diff --git a/tests/components/onewire/test_select.py b/tests/components/onewire/test_select.py new file mode 100644 index 00000000000..0a594e2c076 --- /dev/null +++ b/tests/components/onewire/test_select.py @@ -0,0 +1,67 @@ +"""Tests for 1-Wire selects.""" + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.onewire._PLATFORMS", [Platform.SELECT]): + yield + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_selects( + hass: HomeAssistant, + config_entry: MockConfigEntry, + owproxy: MagicMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test for 1-Wire select entities.""" + setup_owproxy_mock_devices(owproxy, MOCK_OWPROXY_DEVICES.keys()) + await hass.config_entries.async_setup(config_entry.entry_id) + + await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + + +@pytest.mark.parametrize("device_id", ["28.111111111111"]) +async def test_selection_option_service( + hass: HomeAssistant, + config_entry: MockConfigEntry, + owproxy: MagicMock, + device_id: str, +) -> None: + """Test for 1-Wire select option service.""" + setup_owproxy_mock_devices(owproxy, [device_id]) + await hass.config_entries.async_setup(config_entry.entry_id) + + entity_id = "select.28_111111111111_temperature_resolution" + assert hass.states.get(entity_id).state == "12" + + # Test SELECT_OPTION service + owproxy.return_value.read.side_effect = [b" 9"] + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "9"}, + blocking=True, + ) + assert hass.states.get(entity_id).state == "9"