mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Add additional number entities to IronOS (#131943)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
11a2a62144
commit
ea7f1b2a4e
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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 = (
|
||||
|
@ -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": {
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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": {
|
||||
|
@ -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)]
|
||||
|
@ -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
@ -502,7 +502,7 @@
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 3,
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user