From ea7f1b2a4e245da5e1607436227ea9a7e473803d Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:35:36 +0100 Subject: [PATCH] Add additional number entities to IronOS (#131943) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/iron_os/__init__.py | 25 +- .../components/iron_os/coordinator.py | 73 +- homeassistant/components/iron_os/entity.py | 11 +- homeassistant/components/iron_os/icons.json | 57 + homeassistant/components/iron_os/number.py | 328 +++++- homeassistant/components/iron_os/sensor.py | 4 +- homeassistant/components/iron_os/strings.json | 54 + homeassistant/components/iron_os/update.py | 2 +- tests/components/iron_os/conftest.py | 29 +- .../iron_os/snapshots/test_number.ambr | 1009 +++++++++++++++++ .../iron_os/snapshots/test_sensor.ambr | 2 +- tests/components/iron_os/test_init.py | 33 +- tests/components/iron_os/test_number.py | 104 +- 13 files changed, 1692 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/iron_os/__init__.py b/homeassistant/components/iron_os/__init__.py index 56a83117e68..35b426d11ab 100644 --- a/homeassistant/components/iron_os/__init__.py +++ b/homeassistant/components/iron_os/__init__.py @@ -19,15 +19,22 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util.hass_dict import HassKey from .const import DOMAIN -from .coordinator import IronOSFirmwareUpdateCoordinator, IronOSLiveDataCoordinator +from .coordinator import ( + IronOSCoordinators, + IronOSFirmwareUpdateCoordinator, + IronOSLiveDataCoordinator, + IronOSSettingsCoordinator, +) PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.UPDATE] -type IronOSConfigEntry = ConfigEntry[IronOSLiveDataCoordinator] +CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +type IronOSConfigEntry = ConfigEntry[IronOSCoordinators] IRON_OS_KEY: HassKey[IronOSFirmwareUpdateCoordinator] = HassKey(DOMAIN) -CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -59,10 +66,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: IronOSConfigEntry) -> bo device = Pynecil(ble_device) - coordinator = IronOSLiveDataCoordinator(hass, device) - await coordinator.async_config_entry_first_refresh() + live_data = IronOSLiveDataCoordinator(hass, device) + await live_data.async_config_entry_first_refresh() - entry.runtime_data = coordinator + settings = IronOSSettingsCoordinator(hass, device) + await settings.async_config_entry_first_refresh() + + entry.runtime_data = IronOSCoordinators( + live_data=live_data, + settings=settings, + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/iron_os/coordinator.py b/homeassistant/components/iron_os/coordinator.py index 699f5a01704..cfd40d66ac7 100644 --- a/homeassistant/components/iron_os/coordinator.py +++ b/homeassistant/components/iron_os/coordinator.py @@ -2,15 +2,23 @@ from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging from typing import TYPE_CHECKING from aiogithubapi import GitHubAPI, GitHubException, GitHubReleaseModel -from pynecil import CommunicationError, DeviceInfoResponse, LiveDataResponse, Pynecil +from pynecil import ( + CommunicationError, + DeviceInfoResponse, + LiveDataResponse, + Pynecil, + SettingsDataResponse, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -19,24 +27,58 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=5) SCAN_INTERVAL_GITHUB = timedelta(hours=3) +SCAN_INTERVAL_SETTINGS = timedelta(seconds=60) -class IronOSLiveDataCoordinator(DataUpdateCoordinator[LiveDataResponse]): - """IronOS live data coordinator.""" +@dataclass +class IronOSCoordinators: + """IronOS data class holding coordinators.""" + + live_data: IronOSLiveDataCoordinator + settings: IronOSSettingsCoordinator + + +class IronOSBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]): + """IronOS base coordinator.""" device_info: DeviceInfoResponse config_entry: ConfigEntry - def __init__(self, hass: HomeAssistant, device: Pynecil) -> None: + def __init__( + self, + hass: HomeAssistant, + device: Pynecil, + update_interval: timedelta, + ) -> None: """Initialize IronOS coordinator.""" + super().__init__( hass, _LOGGER, name=DOMAIN, - update_interval=SCAN_INTERVAL, + update_interval=update_interval, + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=3, immediate=False + ), ) self.device = device + async def _async_setup(self) -> None: + """Set up the coordinator.""" + try: + self.device_info = await self.device.get_device_info() + + except CommunicationError as e: + raise UpdateFailed("Cannot connect to device") from e + + +class IronOSLiveDataCoordinator(IronOSBaseCoordinator): + """IronOS coordinator.""" + + def __init__(self, hass: HomeAssistant, device: Pynecil) -> None: + """Initialize IronOS coordinator.""" + super().__init__(hass, device=device, update_interval=SCAN_INTERVAL) + async def _async_update_data(self) -> LiveDataResponse: """Fetch data from Device.""" @@ -80,3 +122,24 @@ class IronOSFirmwareUpdateCoordinator(DataUpdateCoordinator[GitHubReleaseModel]) assert release.data return release.data + + +class IronOSSettingsCoordinator(IronOSBaseCoordinator): + """IronOS coordinator.""" + + def __init__(self, hass: HomeAssistant, device: Pynecil) -> None: + """Initialize IronOS coordinator.""" + super().__init__(hass, device=device, update_interval=SCAN_INTERVAL_SETTINGS) + + async def _async_update_data(self) -> SettingsDataResponse: + """Fetch data from Device.""" + + characteristics = set(self.async_contexts()) + + if self.device.is_connected and characteristics: + try: + return await self.device.get_settings(list(characteristics)) + except CommunicationError as e: + _LOGGER.debug("Failed to fetch settings", exc_info=e) + + return self.data or SettingsDataResponse() diff --git a/homeassistant/components/iron_os/entity.py b/homeassistant/components/iron_os/entity.py index 77bebda9390..684957a2197 100644 --- a/homeassistant/components/iron_os/entity.py +++ b/homeassistant/components/iron_os/entity.py @@ -2,28 +2,29 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import MANUFACTURER, MODEL -from .coordinator import IronOSLiveDataCoordinator +from .coordinator import IronOSBaseCoordinator -class IronOSBaseEntity(CoordinatorEntity[IronOSLiveDataCoordinator]): +class IronOSBaseEntity(CoordinatorEntity[IronOSBaseCoordinator]): """Base IronOS entity.""" _attr_has_entity_name = True def __init__( self, - coordinator: IronOSLiveDataCoordinator, + coordinator: IronOSBaseCoordinator, entity_description: EntityDescription, + context: Any | None = None, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) + super().__init__(coordinator, context=context) self.entity_description = entity_description self._attr_unique_id = ( diff --git a/homeassistant/components/iron_os/icons.json b/homeassistant/components/iron_os/icons.json index fa14b8134d0..24d27457689 100644 --- a/homeassistant/components/iron_os/icons.json +++ b/homeassistant/components/iron_os/icons.json @@ -3,6 +3,63 @@ "number": { "setpoint_temperature": { "default": "mdi:thermometer" + }, + "sleep_temperature": { + "default": "mdi:thermometer-low" + }, + "sleep_timeout": { + "default": "mdi:timer-sand" + }, + "qc_max_voltage": { + "default": "mdi:flash-alert-outline" + }, + "pd_timeout": { + "default": "mdi:timer-alert-outline" + }, + "boost_temp": { + "default": "mdi:thermometer-high" + }, + "shutdown_timeout": { + "default": "mdi:thermometer-off" + }, + "display_brightness": { + "default": "mdi:brightness-6" + }, + "voltage_div": { + "default": "mdi:call-split" + }, + "temp_increment_short": { + "default": "mdi:gesture-tap-button" + }, + "temp_increment_long": { + "default": "mdi:gesture-tap-button" + }, + "accel_sensitivity": { + "default": "mdi:motion" + }, + "calibration_offset": { + "default": "mdi:contrast" + }, + "hall_sensitivity": { + "default": "mdi:leak" + }, + "keep_awake_pulse_delay": { + "default": "mdi:clock-end" + }, + "keep_awake_pulse_duration": { + "default": "mdi:clock-start" + }, + "keep_awake_pulse_power": { + "default": "mdi:waves-arrow-up" + }, + "min_voltage_per_cell": { + "default": "mdi:fuel-cell" + }, + "min_dc_voltage_cells": { + "default": "mdi:battery-arrow-down" + }, + "power_limit": { + "default": "mdi:flash-alert" } }, "sensor": { diff --git a/homeassistant/components/iron_os/number.py b/homeassistant/components/iron_os/number.py index 2da80aac327..a288a61b021 100644 --- a/homeassistant/components/iron_os/number.py +++ b/homeassistant/components/iron_os/number.py @@ -6,21 +6,34 @@ from collections.abc import Callable from dataclasses import dataclass from enum import StrEnum -from pynecil import CharSetting, CommunicationError, LiveDataResponse +from pynecil import ( + CharSetting, + CommunicationError, + LiveDataResponse, + SettingsDataResponse, +) from homeassistant.components.number import ( + DEFAULT_MAX_VALUE, NumberDeviceClass, NumberEntity, NumberEntityDescription, NumberMode, ) -from homeassistant.const import UnitOfTemperature +from homeassistant.const import ( + EntityCategory, + UnitOfElectricPotential, + UnitOfPower, + UnitOfTemperature, + UnitOfTime, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import IronOSConfigEntry from .const import DOMAIN, MAX_TEMP, MIN_TEMP +from .coordinator import IronOSCoordinators from .entity import IronOSBaseEntity PARALLEL_UPDATES = 0 @@ -30,15 +43,39 @@ PARALLEL_UPDATES = 0 class IronOSNumberEntityDescription(NumberEntityDescription): """Describes IronOS number entity.""" - value_fn: Callable[[LiveDataResponse], float | int | None] - max_value_fn: Callable[[LiveDataResponse], float | int] - set_key: CharSetting + value_fn: Callable[[LiveDataResponse, SettingsDataResponse], float | int | None] + max_value_fn: Callable[[LiveDataResponse], float | int] | None = None + characteristic: CharSetting + raw_value_fn: Callable[[float], float | int] | None = None class PinecilNumber(StrEnum): """Number controls for Pinecil device.""" SETPOINT_TEMP = "setpoint_temperature" + SLEEP_TEMP = "sleep_temperature" + SLEEP_TIMEOUT = "sleep_timeout" + QC_MAX_VOLTAGE = "qc_max_voltage" + PD_TIMEOUT = "pd_timeout" + BOOST_TEMP = "boost_temp" + SHUTDOWN_TIMEOUT = "shutdown_timeout" + DISPLAY_BRIGHTNESS = "display_brightness" + POWER_LIMIT = "power_limit" + CALIBRATION_OFFSET = "calibration_offset" + HALL_SENSITIVITY = "hall_sensitivity" + MIN_VOLTAGE_PER_CELL = "min_voltage_per_cell" + ACCEL_SENSITIVITY = "accel_sensitivity" + KEEP_AWAKE_PULSE_POWER = "keep_awake_pulse_power" + KEEP_AWAKE_PULSE_DELAY = "keep_awake_pulse_delay" + KEEP_AWAKE_PULSE_DURATION = "keep_awake_pulse_duration" + VOLTAGE_DIV = "voltage_div" + TEMP_INCREMENT_SHORT = "temp_increment_short" + TEMP_INCREMENT_LONG = "temp_increment_long" + + +def multiply(value: float | None, multiplier: float) -> float | None: + """Multiply if not None.""" + return value * multiplier if value is not None else None PINECIL_NUMBER_DESCRIPTIONS: tuple[IronOSNumberEntityDescription, ...] = ( @@ -47,13 +84,249 @@ PINECIL_NUMBER_DESCRIPTIONS: tuple[IronOSNumberEntityDescription, ...] = ( translation_key=PinecilNumber.SETPOINT_TEMP, native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=NumberDeviceClass.TEMPERATURE, - value_fn=lambda data: data.setpoint_temp, - set_key=CharSetting.SETPOINT_TEMP, + value_fn=lambda data, _: data.setpoint_temp, + characteristic=CharSetting.SETPOINT_TEMP, mode=NumberMode.BOX, native_min_value=MIN_TEMP, native_step=5, max_value_fn=lambda data: min(data.max_tip_temp_ability or MAX_TEMP, MAX_TEMP), ), + IronOSNumberEntityDescription( + key=PinecilNumber.SLEEP_TEMP, + translation_key=PinecilNumber.SLEEP_TEMP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=NumberDeviceClass.TEMPERATURE, + value_fn=lambda _, settings: settings.get("sleep_temp"), + characteristic=CharSetting.SLEEP_TEMP, + mode=NumberMode.BOX, + native_min_value=MIN_TEMP, + native_max_value=MAX_TEMP, + native_step=10, + entity_category=EntityCategory.CONFIG, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.BOOST_TEMP, + translation_key=PinecilNumber.BOOST_TEMP, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=NumberDeviceClass.TEMPERATURE, + value_fn=lambda _, settings: settings.get("boost_temp"), + characteristic=CharSetting.BOOST_TEMP, + mode=NumberMode.BOX, + native_min_value=0, + native_max_value=MAX_TEMP, + native_step=10, + entity_category=EntityCategory.CONFIG, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.QC_MAX_VOLTAGE, + translation_key=PinecilNumber.QC_MAX_VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=NumberDeviceClass.VOLTAGE, + value_fn=lambda _, settings: settings.get("qc_ideal_voltage"), + characteristic=CharSetting.QC_IDEAL_VOLTAGE, + mode=NumberMode.BOX, + native_min_value=9.0, + native_max_value=22.0, + native_step=0.1, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.PD_TIMEOUT, + translation_key=PinecilNumber.PD_TIMEOUT, + native_unit_of_measurement=UnitOfTime.SECONDS, + device_class=NumberDeviceClass.DURATION, + value_fn=lambda _, settings: settings.get("pd_negotiation_timeout"), + characteristic=CharSetting.PD_NEGOTIATION_TIMEOUT, + mode=NumberMode.BOX, + native_min_value=0, + native_max_value=5.0, + native_step=1, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.SHUTDOWN_TIMEOUT, + translation_key=PinecilNumber.SHUTDOWN_TIMEOUT, + native_unit_of_measurement=UnitOfTime.MINUTES, + device_class=NumberDeviceClass.DURATION, + value_fn=lambda _, settings: settings.get("shutdown_time"), + characteristic=CharSetting.SHUTDOWN_TIME, + mode=NumberMode.BOX, + native_min_value=0, + native_max_value=60, + native_step=1, + entity_category=EntityCategory.CONFIG, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.DISPLAY_BRIGHTNESS, + translation_key=PinecilNumber.DISPLAY_BRIGHTNESS, + value_fn=lambda _, settings: settings.get("display_brightness"), + characteristic=CharSetting.DISPLAY_BRIGHTNESS, + mode=NumberMode.SLIDER, + native_min_value=1, + native_max_value=5, + native_step=1, + entity_category=EntityCategory.CONFIG, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.SLEEP_TIMEOUT, + translation_key=PinecilNumber.SLEEP_TIMEOUT, + value_fn=lambda _, settings: settings.get("sleep_timeout"), + characteristic=CharSetting.SLEEP_TIMEOUT, + mode=NumberMode.BOX, + native_min_value=0, + native_max_value=15, + native_step=1, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfTime.MINUTES, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.POWER_LIMIT, + translation_key=PinecilNumber.POWER_LIMIT, + value_fn=lambda _, settings: settings.get("power_limit"), + characteristic=CharSetting.POWER_LIMIT, + mode=NumberMode.BOX, + native_min_value=0, + native_max_value=12, + native_step=0.1, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfPower.WATT, + entity_registry_enabled_default=False, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.CALIBRATION_OFFSET, + translation_key=PinecilNumber.CALIBRATION_OFFSET, + value_fn=lambda _, settings: settings.get("calibration_offset"), + characteristic=CharSetting.CALIBRATION_OFFSET, + mode=NumberMode.BOX, + native_min_value=100, + native_max_value=2500, + native_step=1, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfElectricPotential.MICROVOLT, + entity_registry_enabled_default=False, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.HALL_SENSITIVITY, + translation_key=PinecilNumber.HALL_SENSITIVITY, + value_fn=lambda _, settings: settings.get("hall_sensitivity"), + characteristic=CharSetting.HALL_SENSITIVITY, + mode=NumberMode.SLIDER, + native_min_value=0, + native_max_value=9, + native_step=1, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.MIN_VOLTAGE_PER_CELL, + translation_key=PinecilNumber.MIN_VOLTAGE_PER_CELL, + value_fn=lambda _, settings: settings.get("min_voltage_per_cell"), + characteristic=CharSetting.MIN_VOLTAGE_PER_CELL, + mode=NumberMode.BOX, + native_min_value=2.4, + native_max_value=3.8, + native_step=0.1, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_registry_enabled_default=False, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.ACCEL_SENSITIVITY, + translation_key=PinecilNumber.ACCEL_SENSITIVITY, + value_fn=lambda _, settings: settings.get("accel_sensitivity"), + characteristic=CharSetting.ACCEL_SENSITIVITY, + mode=NumberMode.SLIDER, + native_min_value=0, + native_max_value=9, + native_step=1, + entity_category=EntityCategory.CONFIG, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.KEEP_AWAKE_PULSE_POWER, + translation_key=PinecilNumber.KEEP_AWAKE_PULSE_POWER, + value_fn=lambda _, settings: settings.get("keep_awake_pulse_power"), + characteristic=CharSetting.KEEP_AWAKE_PULSE_POWER, + mode=NumberMode.BOX, + native_min_value=0, + native_max_value=9.9, + native_step=0.1, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfPower.WATT, + entity_registry_enabled_default=False, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.KEEP_AWAKE_PULSE_DELAY, + translation_key=PinecilNumber.KEEP_AWAKE_PULSE_DELAY, + value_fn=( + lambda _, settings: multiply(settings.get("keep_awake_pulse_delay"), 2.5) + ), + characteristic=CharSetting.KEEP_AWAKE_PULSE_DELAY, + raw_value_fn=lambda value: value / 2.5, + mode=NumberMode.BOX, + native_min_value=2.5, + native_max_value=22.5, + native_step=2.5, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfTime.SECONDS, + entity_registry_enabled_default=False, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.KEEP_AWAKE_PULSE_DURATION, + translation_key=PinecilNumber.KEEP_AWAKE_PULSE_DURATION, + value_fn=( + lambda _, settings: multiply(settings.get("keep_awake_pulse_duration"), 250) + ), + characteristic=CharSetting.KEEP_AWAKE_PULSE_DURATION, + raw_value_fn=lambda value: value / 250, + mode=NumberMode.BOX, + native_min_value=250, + native_max_value=2250, + native_step=250, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + entity_registry_enabled_default=False, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.VOLTAGE_DIV, + translation_key=PinecilNumber.VOLTAGE_DIV, + value_fn=(lambda _, settings: settings.get("voltage_div")), + characteristic=CharSetting.VOLTAGE_DIV, + raw_value_fn=lambda value: value, + mode=NumberMode.BOX, + native_min_value=360, + native_max_value=900, + native_step=1, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.TEMP_INCREMENT_SHORT, + translation_key=PinecilNumber.TEMP_INCREMENT_SHORT, + value_fn=(lambda _, settings: settings.get("temp_increment_short")), + characteristic=CharSetting.TEMP_INCREMENT_SHORT, + raw_value_fn=lambda value: value, + mode=NumberMode.BOX, + native_min_value=1, + native_max_value=50, + native_step=1, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + IronOSNumberEntityDescription( + key=PinecilNumber.TEMP_INCREMENT_LONG, + translation_key=PinecilNumber.TEMP_INCREMENT_LONG, + value_fn=(lambda _, settings: settings.get("temp_increment_long")), + characteristic=CharSetting.TEMP_INCREMENT_LONG, + raw_value_fn=lambda value: value, + mode=NumberMode.BOX, + native_min_value=5, + native_max_value=90, + native_step=5, + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), ) @@ -76,23 +349,56 @@ class IronOSNumberEntity(IronOSBaseEntity, NumberEntity): entity_description: IronOSNumberEntityDescription + def __init__( + self, + coordinator: IronOSCoordinators, + entity_description: IronOSNumberEntityDescription, + ) -> None: + """Initialize the number entity.""" + super().__init__( + coordinator.live_data, entity_description, entity_description.characteristic + ) + + self.settings = coordinator.settings + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" + if raw_value_fn := self.entity_description.raw_value_fn: + value = raw_value_fn(value) try: - await self.coordinator.device.write(self.entity_description.set_key, value) + await self.coordinator.device.write( + self.entity_description.characteristic, value + ) except CommunicationError as e: raise ServiceValidationError( translation_domain=DOMAIN, translation_key="submit_setting_failed", ) from e - self.async_write_ha_state() + await self.settings.async_request_refresh() @property def native_value(self) -> float | int | None: """Return sensor state.""" - return self.entity_description.value_fn(self.coordinator.data) + return self.entity_description.value_fn( + self.coordinator.data, self.settings.data + ) @property def native_max_value(self) -> float: """Return sensor state.""" - return self.entity_description.max_value_fn(self.coordinator.data) + + if self.entity_description.max_value_fn is not None: + return self.entity_description.max_value_fn(self.coordinator.data) + + return self.entity_description.native_max_value or DEFAULT_MAX_VALUE + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + + await super().async_added_to_hass() + self.async_on_remove( + self.settings.async_add_listener( + self._handle_coordinator_update, self.entity_description.characteristic + ) + ) + await self.settings.async_request_refresh() diff --git a/homeassistant/components/iron_os/sensor.py b/homeassistant/components/iron_os/sensor.py index b21fa2e5591..05d56db26d3 100644 --- a/homeassistant/components/iron_os/sensor.py +++ b/homeassistant/components/iron_os/sensor.py @@ -141,7 +141,7 @@ PINECIL_SENSOR_DESCRIPTIONS: tuple[IronOSSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfElectricPotential.MICROVOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, - suggested_display_precision=3, + suggested_display_precision=0, value_fn=lambda data: data.tip_voltage, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -181,7 +181,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors from a config entry.""" - coordinator = entry.runtime_data + coordinator = entry.runtime_data.live_data async_add_entities( IronOSSensorEntity(coordinator, description) diff --git a/homeassistant/components/iron_os/strings.json b/homeassistant/components/iron_os/strings.json index 92441b39fc3..c474b704677 100644 --- a/homeassistant/components/iron_os/strings.json +++ b/homeassistant/components/iron_os/strings.json @@ -23,6 +23,60 @@ "number": { "setpoint_temperature": { "name": "Setpoint temperature" + }, + "sleep_temperature": { + "name": "Sleep temperature" + }, + "sleep_timeout": { + "name": "Sleep timeout" + }, + "qc_max_voltage": { + "name": "Quick Charge voltage" + }, + "pd_timeout": { + "name": "Power Delivery timeout" + }, + "boost_temp": { + "name": "Boost temperature" + }, + "shutdown_timeout": { + "name": "Shutdown timeout" + }, + "display_brightness": { + "name": "Display brightness" + }, + "power_limit": { + "name": "Power limit" + }, + "calibration_offset": { + "name": "Calibration offset" + }, + "hall_sensitivity": { + "name": "Hall effect sensitivity" + }, + "min_voltage_per_cell": { + "name": "Min. voltage per cell" + }, + "accel_sensitivity": { + "name": "Motion sensitivity" + }, + "keep_awake_pulse_power": { + "name": "Keep-awake pulse intensity" + }, + "keep_awake_pulse_delay": { + "name": "Keep-awake pulse delay" + }, + "keep_awake_pulse_duration": { + "name": "Keep-awake pulse duration" + }, + "voltage_div": { + "name": "Voltage divider" + }, + "temp_increment_short": { + "name": "Short-press temperature step" + }, + "temp_increment_long": { + "name": "Long-press temperature step" } }, "sensor": { diff --git a/homeassistant/components/iron_os/update.py b/homeassistant/components/iron_os/update.py index 0da0786821e..b431d321f24 100644 --- a/homeassistant/components/iron_os/update.py +++ b/homeassistant/components/iron_os/update.py @@ -30,7 +30,7 @@ async def async_setup_entry( ) -> None: """Set up IronOS update platform.""" - coordinator = entry.runtime_data + coordinator = entry.runtime_data.live_data async_add_entities( [IronOSUpdate(coordinator, hass.data[IRON_OS_KEY], UPDATE_DESCRIPTION)] diff --git a/tests/components/iron_os/conftest.py b/tests/components/iron_os/conftest.py index a7c3592ae73..eda9c2c5d1d 100644 --- a/tests/components/iron_os/conftest.py +++ b/tests/components/iron_os/conftest.py @@ -5,7 +5,13 @@ from unittest.mock import AsyncMock, MagicMock, patch from bleak.backends.device import BLEDevice from habluetooth import BluetoothServiceInfoBleak -from pynecil import DeviceInfoResponse, LiveDataResponse, OperatingMode, PowerSource +from pynecil import ( + DeviceInfoResponse, + LiveDataResponse, + OperatingMode, + PowerSource, + SettingsDataResponse, +) import pytest from homeassistant.components.iron_os import DOMAIN @@ -145,6 +151,27 @@ def mock_pynecil() -> Generator[AsyncMock]: device_sn="0000c0ffeec0ffee", name=DEFAULT_NAME, ) + client.get_settings.return_value = SettingsDataResponse( + sleep_temp=150, + sleep_timeout=5, + min_dc_voltage_cells=0, + min_volltage_per_cell=3.3, + qc_ideal_voltage=9.0, + accel_sensitivity=7, + shutdown_time=10, + keep_awake_pulse_power=0.5, + keep_awake_pulse_delay=4, + keep_awake_pulse_duration=1, + voltage_div=600, + boost_temp=420, + calibration_offset=900, + power_limit=12.0, + temp_increment_long=10, + temp_increment_short=1, + hall_sensitivity=7, + pd_negotiation_timeout=2.0, + display_brightness=3, + ) client.get_live_data.return_value = LiveDataResponse( live_temp=298, setpoint_temp=300, diff --git a/tests/components/iron_os/snapshots/test_number.ambr b/tests/components/iron_os/snapshots/test_number.ambr index 2f5ee62e37e..24663cc4b0f 100644 --- a/tests/components/iron_os/snapshots/test_number.ambr +++ b/tests/components/iron_os/snapshots/test_number.ambr @@ -1,4 +1,732 @@ # serializer version: 1 +# name: test_state[number.pinecil_boost_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 450, + 'min': 0, + 'mode': , + 'step': 10, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_boost_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Boost temperature', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_boost_temp', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_boost_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pinecil Boost temperature', + 'max': 450, + 'min': 0, + 'mode': , + 'step': 10, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_boost_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '420', + }) +# --- +# name: test_state[number.pinecil_calibration_offset-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 2500, + 'min': 100, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_calibration_offset', + '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': 'Calibration offset', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_calibration_offset', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_calibration_offset-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Calibration offset', + 'max': 2500, + 'min': 100, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_calibration_offset', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '900', + }) +# --- +# name: test_state[number.pinecil_display_brightness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 5, + 'min': 1, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_display_brightness', + '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': 'Display brightness', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_display_brightness', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[number.pinecil_display_brightness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Display brightness', + 'max': 5, + 'min': 1, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.pinecil_display_brightness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_state[number.pinecil_hall_effect_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 9, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_hall_effect_sensitivity', + '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': 'Hall effect sensitivity', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_hall_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[number.pinecil_hall_effect_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Hall effect sensitivity', + 'max': 9, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.pinecil_hall_effect_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- +# name: test_state[number.pinecil_keep_awake_pulse_delay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 22.5, + 'min': 2.5, + 'mode': , + 'step': 2.5, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_keep_awake_pulse_delay', + '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': 'Keep-awake pulse delay', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_keep_awake_pulse_delay', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_keep_awake_pulse_delay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Keep-awake pulse delay', + 'max': 22.5, + 'min': 2.5, + 'mode': , + 'step': 2.5, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_keep_awake_pulse_delay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.0', + }) +# --- +# name: test_state[number.pinecil_keep_awake_pulse_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 2250, + 'min': 250, + 'mode': , + 'step': 250, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_keep_awake_pulse_duration', + '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': 'Keep-awake pulse duration', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_keep_awake_pulse_duration', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_keep_awake_pulse_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Keep-awake pulse duration', + 'max': 2250, + 'min': 250, + 'mode': , + 'step': 250, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_keep_awake_pulse_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '250', + }) +# --- +# name: test_state[number.pinecil_keep_awake_pulse_intensity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 9.9, + 'min': 0, + 'mode': , + 'step': 0.1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_keep_awake_pulse_intensity', + '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': 'Keep-awake pulse intensity', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_keep_awake_pulse_power', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_keep_awake_pulse_intensity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Keep-awake pulse intensity', + 'max': 9.9, + 'min': 0, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_keep_awake_pulse_intensity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.5', + }) +# --- +# name: test_state[number.pinecil_long_press_temperature_step-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 90, + 'min': 5, + 'mode': , + 'step': 5, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_long_press_temperature_step', + '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': 'Long-press temperature step', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_temp_increment_long', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_long_press_temperature_step-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Long-press temperature step', + 'max': 90, + 'min': 5, + 'mode': , + 'step': 5, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_long_press_temperature_step', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_state[number.pinecil_min_voltage_per_cell-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 3.8, + 'min': 2.4, + 'mode': , + 'step': 0.1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_min_voltage_per_cell', + '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': 'Min. voltage per cell', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_min_voltage_per_cell', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_min_voltage_per_cell-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Min. voltage per cell', + 'max': 3.8, + 'min': 2.4, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_min_voltage_per_cell', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_state[number.pinecil_motion_sensitivity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 9, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_motion_sensitivity', + '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': 'Motion sensitivity', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_accel_sensitivity', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[number.pinecil_motion_sensitivity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Motion sensitivity', + 'max': 9, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.pinecil_motion_sensitivity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '7', + }) +# --- +# name: test_state[number.pinecil_power_delivery_timeout-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 5.0, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_power_delivery_timeout', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power Delivery timeout', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_pd_timeout', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_power_delivery_timeout-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Pinecil Power Delivery timeout', + 'max': 5.0, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_power_delivery_timeout', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.0', + }) +# --- +# name: test_state[number.pinecil_power_limit-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 12, + 'min': 0, + 'mode': , + 'step': 0.1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_power_limit', + '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': 'Power limit', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_power_limit', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_power_limit-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Power limit', + 'max': 12, + 'min': 0, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_power_limit', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12.0', + }) +# --- +# name: test_state[number.pinecil_quick_charge_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 22.0, + 'min': 9.0, + 'mode': , + 'step': 0.1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_quick_charge_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Quick Charge voltage', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_qc_max_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_quick_charge_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Pinecil Quick Charge voltage', + 'max': 22.0, + 'min': 9.0, + 'mode': , + 'step': 0.1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_quick_charge_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9.0', + }) +# --- # name: test_state[number.pinecil_setpoint_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -56,3 +784,284 @@ 'state': '300', }) # --- +# name: test_state[number.pinecil_short_press_temperature_step-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 50, + 'min': 1, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_short_press_temperature_step', + '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': 'Short-press temperature step', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_temp_increment_short', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_short_press_temperature_step-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Short-press temperature step', + 'max': 50, + 'min': 1, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_short_press_temperature_step', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_state[number.pinecil_shutdown_timeout-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 60, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_shutdown_timeout', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Shutdown timeout', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_shutdown_timeout', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_shutdown_timeout-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Pinecil Shutdown timeout', + 'max': 60, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_shutdown_timeout', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_state[number.pinecil_sleep_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 450, + 'min': 10, + 'mode': , + 'step': 10, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_sleep_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Sleep temperature', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_sleep_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_sleep_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pinecil Sleep temperature', + 'max': 450, + 'min': 10, + 'mode': , + 'step': 10, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_sleep_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '150', + }) +# --- +# name: test_state[number.pinecil_sleep_timeout-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 15, + 'min': 0, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_sleep_timeout', + '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': 'Sleep timeout', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_sleep_timeout', + 'unit_of_measurement': , + }) +# --- +# name: test_state[number.pinecil_sleep_timeout-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Sleep timeout', + 'max': 15, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.pinecil_sleep_timeout', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_state[number.pinecil_voltage_divider-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 900, + 'min': 360, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.pinecil_voltage_divider', + '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': 'Voltage divider', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_voltage_div', + 'unit_of_measurement': None, + }) +# --- +# name: test_state[number.pinecil_voltage_divider-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Pinecil Voltage divider', + 'max': 900, + 'min': 360, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.pinecil_voltage_divider', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '600', + }) +# --- diff --git a/tests/components/iron_os/snapshots/test_sensor.ambr b/tests/components/iron_os/snapshots/test_sensor.ambr index 44a17dd6ea5..9ab5d47eec8 100644 --- a/tests/components/iron_os/snapshots/test_sensor.ambr +++ b/tests/components/iron_os/snapshots/test_sensor.ambr @@ -502,7 +502,7 @@ 'name': None, 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 3, + 'suggested_display_precision': 0, }), }), 'original_device_class': , diff --git a/tests/components/iron_os/test_init.py b/tests/components/iron_os/test_init.py index f7db2a813ec..21194a55eea 100644 --- a/tests/components/iron_os/test_init.py +++ b/tests/components/iron_os/test_init.py @@ -1,14 +1,17 @@ """Test init of IronOS integration.""" +from datetime import datetime, timedelta from unittest.mock import AsyncMock +from freezegun.api import FrozenDateTimeFactory from pynecil import CommunicationError import pytest from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.usefixtures("mock_pynecil", "ble_device") @@ -45,16 +48,42 @@ async def test_update_data_config_entry_not_ready( assert config_entry.state is ConfigEntryState.SETUP_RETRY -@pytest.mark.usefixtures("ble_device") +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") async def test_setup_config_entry_not_ready( hass: HomeAssistant, config_entry: MockConfigEntry, mock_pynecil: AsyncMock, + freezer: FrozenDateTimeFactory, ) -> None: """Test config entry not ready.""" + mock_pynecil.get_settings.side_effect = CommunicationError mock_pynecil.get_device_info.side_effect = CommunicationError config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + freezer.tick(timedelta(seconds=60)) + async_fire_time_changed(hass) + await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") +async def test_settings_exception( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pynecil: AsyncMock, +) -> None: + """Test skipping of settings on exception.""" + mock_pynecil.get_settings.side_effect = CommunicationError + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + async_fire_time_changed(hass, datetime.now() + timedelta(seconds=60)) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.LOADED + + assert (state := hass.states.get("number.pinecil_boost_temperature")) + assert state.state == STATE_UNKNOWN diff --git a/tests/components/iron_os/test_number.py b/tests/components/iron_os/test_number.py index 781492987ee..e0617a5012f 100644 --- a/tests/components/iron_os/test_number.py +++ b/tests/components/iron_os/test_number.py @@ -1,8 +1,10 @@ """Tests for the IronOS number platform.""" from collections.abc import AsyncGenerator +from datetime import timedelta from unittest.mock import AsyncMock, patch +from freezegun.api import FrozenDateTimeFactory from pynecil import CharSetting, CommunicationError import pytest from syrupy.assertion import SnapshotAssertion @@ -18,11 +20,11 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform @pytest.fixture(autouse=True) -async def sensor_only() -> AsyncGenerator[None]: +async def number_only() -> AsyncGenerator[None]: """Enable only the number platform.""" with patch( "homeassistant.components.iron_os.PLATFORMS", @@ -39,6 +41,7 @@ async def test_state( config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test the IronOS number platform states.""" config_entry.add_to_hass(hass) @@ -47,14 +50,105 @@ async def test_state( assert config_entry.state is ConfigEntryState.LOADED + freezer.tick(timedelta(seconds=60)) + async_fire_time_changed(hass) + await hass.async_block_till_done() await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) +@pytest.mark.parametrize( + ("entity_id", "characteristic", "value", "expected_value"), + [ + ( + "number.pinecil_setpoint_temperature", + CharSetting.SETPOINT_TEMP, + 300, + 300, + ), + ( + "number.pinecil_boost_temperature", + CharSetting.BOOST_TEMP, + 420, + 420, + ), + ( + "number.pinecil_calibration_offset", + CharSetting.CALIBRATION_OFFSET, + 600, + 600, + ), + ( + "number.pinecil_display_brightness", + CharSetting.DISPLAY_BRIGHTNESS, + 3, + 3, + ), + ( + "number.pinecil_hall_effect_sensitivity", + CharSetting.HALL_SENSITIVITY, + 7, + 7, + ), + ( + "number.pinecil_keep_awake_pulse_delay", + CharSetting.KEEP_AWAKE_PULSE_DELAY, + 10.0, + 4, + ), + ( + "number.pinecil_keep_awake_pulse_duration", + CharSetting.KEEP_AWAKE_PULSE_DURATION, + 500, + 2, + ), + ( + "number.pinecil_keep_awake_pulse_intensity", + CharSetting.KEEP_AWAKE_PULSE_POWER, + 0.5, + 0.5, + ), + ( + "number.pinecil_long_press_temperature_step", + CharSetting.TEMP_INCREMENT_LONG, + 10, + 10, + ), + ( + "number.pinecil_min_voltage_per_cell", + CharSetting.MIN_VOLTAGE_PER_CELL, + 3.3, + 3.3, + ), + ("number.pinecil_motion_sensitivity", CharSetting.ACCEL_SENSITIVITY, 7, 7), + ( + "number.pinecil_power_delivery_timeout", + CharSetting.PD_NEGOTIATION_TIMEOUT, + 2.0, + 2.0, + ), + ("number.pinecil_power_limit", CharSetting.POWER_LIMIT, 12.0, 12.0), + ("number.pinecil_quick_charge_voltage", CharSetting.QC_IDEAL_VOLTAGE, 9.0, 9.0), + ( + "number.pinecil_short_press_temperature_step", + CharSetting.TEMP_INCREMENT_SHORT, + 1, + 1, + ), + ("number.pinecil_shutdown_timeout", CharSetting.SHUTDOWN_TIME, 10, 10), + ("number.pinecil_sleep_temperature", CharSetting.SLEEP_TEMP, 150, 150), + ("number.pinecil_sleep_timeout", CharSetting.SLEEP_TIMEOUT, 5, 5), + ("number.pinecil_voltage_divider", CharSetting.VOLTAGE_DIV, 600, 600), + ], +) @pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device") async def test_set_value( hass: HomeAssistant, config_entry: MockConfigEntry, mock_pynecil: AsyncMock, + entity_id: str, + characteristic: CharSetting, + value: float, + expected_value: float, ) -> None: """Test the IronOS number platform set value service.""" @@ -67,12 +161,12 @@ async def test_set_value( await hass.services.async_call( NUMBER_DOMAIN, SERVICE_SET_VALUE, - service_data={ATTR_VALUE: 300}, - target={ATTR_ENTITY_ID: "number.pinecil_setpoint_temperature"}, + service_data={ATTR_VALUE: value}, + target={ATTR_ENTITY_ID: entity_id}, blocking=True, ) assert len(mock_pynecil.write.mock_calls) == 1 - mock_pynecil.write.assert_called_once_with(CharSetting.SETPOINT_TEMP, 300) + mock_pynecil.write.assert_called_once_with(characteristic, expected_value) @pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device")