Add additional number entities to IronOS (#131943)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Manu 2024-12-02 12:35:36 +01:00 committed by GitHub
parent 11a2a62144
commit ea7f1b2a4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1692 additions and 39 deletions

View File

@ -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

View File

@ -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()

View File

@ -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 = (

View File

@ -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": {

View File

@ -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()

View File

@ -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)

View File

@ -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": {

View File

@ -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)]

View File

@ -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,

File diff suppressed because it is too large Load Diff

View File

@ -502,7 +502,7 @@
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 3,
'suggested_display_precision': 0,
}),
}),
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,

View File

@ -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

View File

@ -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")