diff --git a/homeassistant/components/ohme/const.py b/homeassistant/components/ohme/const.py index 2b7410dc0eb..308664ba0ad 100644 --- a/homeassistant/components/ohme/const.py +++ b/homeassistant/components/ohme/const.py @@ -3,4 +3,10 @@ from homeassistant.const import Platform DOMAIN = "ohme" -PLATFORMS = [Platform.BUTTON, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BUTTON, + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, + Platform.TIME, +] diff --git a/homeassistant/components/ohme/icons.json b/homeassistant/components/ohme/icons.json index 6d187ff7e8d..a6b04004833 100644 --- a/homeassistant/components/ohme/icons.json +++ b/homeassistant/components/ohme/icons.json @@ -41,6 +41,11 @@ "off": "mdi:sleep-off" } } + }, + "time": { + "target_time": { + "default": "mdi:clock-end" + } } }, "services": { diff --git a/homeassistant/components/ohme/manifest.json b/homeassistant/components/ohme/manifest.json index 98c738cea3c..67c41550491 100644 --- a/homeassistant/components/ohme/manifest.json +++ b/homeassistant/components/ohme/manifest.json @@ -7,5 +7,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "quality_scale": "silver", - "requirements": ["ohme==1.2.4"] + "requirements": ["ohme==1.2.5"] } diff --git a/homeassistant/components/ohme/strings.json b/homeassistant/components/ohme/strings.json index 6ba06c98c44..84f62ba65ab 100644 --- a/homeassistant/components/ohme/strings.json +++ b/homeassistant/components/ohme/strings.json @@ -83,6 +83,11 @@ "sleep_when_inactive": { "name": "Sleep when inactive" } + }, + "time": { + "target_time": { + "name": "Target time" + } } }, "exceptions": { diff --git a/homeassistant/components/ohme/time.py b/homeassistant/components/ohme/time.py new file mode 100644 index 00000000000..a7de913ef8e --- /dev/null +++ b/homeassistant/components/ohme/time.py @@ -0,0 +1,77 @@ +"""Platform for time.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from datetime import time + +from ohme import ApiException, OhmeApiClient + +from homeassistant.components.time import TimeEntity, TimeEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import OhmeConfigEntry +from .const import DOMAIN +from .entity import OhmeEntity, OhmeEntityDescription + +PARALLEL_UPDATES = 1 + + +@dataclass(frozen=True, kw_only=True) +class OhmeTimeDescription(OhmeEntityDescription, TimeEntityDescription): + """Class describing Ohme time entities.""" + + set_fn: Callable[[OhmeApiClient, time], Awaitable[None]] + value_fn: Callable[[OhmeApiClient], time] + + +TIME_DESCRIPTION = [ + OhmeTimeDescription( + key="target_time", + translation_key="target_time", + value_fn=lambda client: time( + hour=client.target_time[0], minute=client.target_time[1] + ), + set_fn=lambda client, value: client.async_set_target( + target_time=(value.hour, value.minute) + ), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: OhmeConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up time entities.""" + coordinators = config_entry.runtime_data + coordinator = coordinators.charge_session_coordinator + + async_add_entities( + OhmeTime(coordinator, description) + for description in TIME_DESCRIPTION + if description.is_supported_fn(coordinator.client) + ) + + +class OhmeTime(OhmeEntity, TimeEntity): + """Generic time entity for Ohme.""" + + entity_description: OhmeTimeDescription + + @property + def native_value(self) -> time: + """Return the current value of the time.""" + return self.entity_description.value_fn(self.coordinator.client) + + async def async_set_value(self, value: time) -> None: + """Set the time value.""" + try: + await self.entity_description.set_fn(self.coordinator.client, value) + except ApiException as e: + raise HomeAssistantError( + translation_key="api_failed", translation_domain=DOMAIN + ) from e + await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index 28b41f4f335..96f276097b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1540,7 +1540,7 @@ odp-amsterdam==6.0.2 oemthermostat==1.1.1 # homeassistant.components.ohme -ohme==1.2.4 +ohme==1.2.5 # homeassistant.components.ollama ollama==0.4.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d169f7e5435..6b1053331cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1288,7 +1288,7 @@ objgraph==3.5.0 odp-amsterdam==6.0.2 # homeassistant.components.ohme -ohme==1.2.4 +ohme==1.2.5 # homeassistant.components.ollama ollama==0.4.7 diff --git a/tests/components/ohme/conftest.py b/tests/components/ohme/conftest.py index 0a774c15143..3d3db730d08 100644 --- a/tests/components/ohme/conftest.py +++ b/tests/components/ohme/conftest.py @@ -55,6 +55,7 @@ def mock_client(): client.power = ChargerPower(0, 0, 0, 0) client.target_soc = 50 + client.target_time = (8, 0) client.battery = 80 client.serial = "chargerid" client.ct_connected = True diff --git a/tests/components/ohme/snapshots/test_time.ambr b/tests/components/ohme/snapshots/test_time.ambr new file mode 100644 index 00000000000..4d9fab20e0b --- /dev/null +++ b/tests/components/ohme/snapshots/test_time.ambr @@ -0,0 +1,47 @@ +# serializer version: 1 +# name: test_time[time.ohme_home_pro_target_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'time', + 'entity_category': None, + 'entity_id': 'time.ohme_home_pro_target_time', + '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': 'Target time', + 'platform': 'ohme', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'target_time', + 'unique_id': 'chargerid_target_time', + 'unit_of_measurement': None, + }) +# --- +# name: test_time[time.ohme_home_pro_target_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ohme Home Pro Target time', + }), + 'context': , + 'entity_id': 'time.ohme_home_pro_target_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '08:00:00', + }) +# --- diff --git a/tests/components/ohme/test_time.py b/tests/components/ohme/test_time.py new file mode 100644 index 00000000000..0562dfa124c --- /dev/null +++ b/tests/components/ohme/test_time.py @@ -0,0 +1,55 @@ +"""Tests for time.""" + +from unittest.mock import MagicMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.components.time import ( + ATTR_TIME, + DOMAIN as TIME_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_time( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test the Ohme sensors.""" + with patch("homeassistant.components.ohme.PLATFORMS", [Platform.TIME]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_set_time( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_client: MagicMock, +) -> None: + """Test the time set.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + TIME_DOMAIN, + SERVICE_SET_VALUE, + service_data={ + ATTR_TIME: "00:00:00", + }, + target={ + ATTR_ENTITY_ID: "time.ohme_home_pro_target_time", + }, + blocking=True, + ) + + assert len(mock_client.async_set_target.mock_calls) == 1