Add support for IronOS v2.23 (#139903)

Add support for IronOS 2.23
pull/139927/head
Manu 2025-03-06 11:23:10 +01:00 committed by GitHub
parent 4f255439eb
commit f2b07ea886
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 273 additions and 11 deletions

View File

@ -8,6 +8,7 @@ from enum import Enum
import logging
from typing import cast
from awesomeversion import AwesomeVersion
from pynecil import (
CharSetting,
CommunicationError,
@ -34,6 +35,8 @@ SCAN_INTERVAL = timedelta(seconds=5)
SCAN_INTERVAL_GITHUB = timedelta(hours=3)
SCAN_INTERVAL_SETTINGS = timedelta(seconds=60)
V223 = AwesomeVersion("v2.23")
@dataclass
class IronOSCoordinators:
@ -72,6 +75,7 @@ class IronOSBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
),
)
self.device = device
self.v223_features = False
async def _async_setup(self) -> None:
"""Set up the coordinator."""
@ -81,6 +85,8 @@ class IronOSBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
except CommunicationError as e:
raise UpdateFailed("Cannot connect to device") from e
self.v223_features = AwesomeVersion(self.device_info.build) >= V223
class IronOSLiveDataCoordinator(IronOSBaseCoordinator[LiveDataResponse]):
"""IronOS coordinator."""

View File

@ -73,6 +73,9 @@
},
"power_limit": {
"default": "mdi:flash-alert"
},
"hall_effect_sleep_time": {
"default": "mdi:timer-sand"
}
},
"select": {
@ -105,6 +108,9 @@
},
"usb_pd_mode": {
"default": "mdi:meter-electric-outline"
},
"tip_type": {
"default": "mdi:pencil-outline"
}
},
"sensor": {
@ -154,7 +160,16 @@
"soldering": "mdi:soldering-iron",
"sleeping": "mdi:sleep",
"settings": "mdi:menu-open",
"debug": "mdi:bug-play"
"debug": "mdi:bug-play",
"soldering_profile": "mdi:chart-box-outline",
"temperature_adjust": "mdi:thermostat-box",
"usb_pd_debug": "mdi:bug-play",
"thermal_runaway": "mdi:fire-alert",
"startup_logo": "mdi:dots-circle",
"cjc_calibration": "mdi:tune-vertical",
"startup_warnings": "mdi:alert",
"initialisation_done": "mdi:check-circle",
"hibernating": "mdi:sleep"
}
},
"estimated_power": {

View File

@ -65,6 +65,7 @@ class PinecilNumber(StrEnum):
VOLTAGE_DIV = "voltage_div"
TEMP_INCREMENT_SHORT = "temp_increment_short"
TEMP_INCREMENT_LONG = "temp_increment_long"
HALL_EFFECT_SLEEP_TIME = "hall_effect_sleep_time"
def multiply(value: float | None, multiplier: float) -> float | None:
@ -323,6 +324,23 @@ PINECIL_NUMBER_DESCRIPTIONS: tuple[IronOSNumberEntityDescription, ...] = (
),
)
PINECIL_NUMBER_DESCRIPTIONS_V223: tuple[IronOSNumberEntityDescription, ...] = (
IronOSNumberEntityDescription(
key=PinecilNumber.HALL_EFFECT_SLEEP_TIME,
translation_key=PinecilNumber.HALL_EFFECT_SLEEP_TIME,
value_fn=(lambda _, settings: settings.get("hall_sleep_time")),
characteristic=CharSetting.HALL_SLEEP_TIME,
raw_value_fn=lambda value: value,
mode=NumberMode.BOX,
native_min_value=0,
native_max_value=60,
native_step=5,
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTime.SECONDS,
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
@ -331,10 +349,13 @@ async def async_setup_entry(
) -> None:
"""Set up number entities from a config entry."""
coordinators = entry.runtime_data
descriptions = PINECIL_NUMBER_DESCRIPTIONS
if coordinators.live_data.v223_features:
descriptions += PINECIL_NUMBER_DESCRIPTIONS_V223
async_add_entities(
IronOSNumberEntity(coordinators, description)
for description in PINECIL_NUMBER_DESCRIPTIONS
IronOSNumberEntity(coordinators, description) for description in descriptions
)

View File

@ -17,6 +17,7 @@ from pynecil import (
ScrollSpeed,
SettingsDataResponse,
TempUnit,
TipType,
USBPDMode,
)
@ -53,6 +54,7 @@ class PinecilSelect(StrEnum):
LOCKING_MODE = "locking_mode"
LOGO_DURATION = "logo_duration"
USB_PD_MODE = "usb_pd_mode"
TIP_TYPE = "tip_type"
def enum_to_str(enum: Enum | None) -> str | None:
@ -138,6 +140,8 @@ PINECIL_SELECT_DESCRIPTIONS: tuple[IronOSSelectEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
),
)
PINECIL_SELECT_DESCRIPTIONS_V222: tuple[IronOSSelectEntityDescription, ...] = (
IronOSSelectEntityDescription(
key=PinecilSelect.USB_PD_MODE,
translation_key=PinecilSelect.USB_PD_MODE,
@ -149,6 +153,27 @@ PINECIL_SELECT_DESCRIPTIONS: tuple[IronOSSelectEntityDescription, ...] = (
entity_registry_enabled_default=False,
),
)
PINECIL_SELECT_DESCRIPTIONS_V223: tuple[IronOSSelectEntityDescription, ...] = (
IronOSSelectEntityDescription(
key=PinecilSelect.USB_PD_MODE,
translation_key=PinecilSelect.USB_PD_MODE,
characteristic=CharSetting.USB_PD_MODE,
value_fn=lambda x: enum_to_str(x.get("usb_pd_mode")),
raw_value_fn=lambda value: USBPDMode[value.upper()],
options=[x.name.lower() for x in USBPDMode],
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
),
IronOSSelectEntityDescription(
key=PinecilSelect.TIP_TYPE,
translation_key=PinecilSelect.TIP_TYPE,
characteristic=CharSetting.TIP_TYPE,
value_fn=lambda x: enum_to_str(x.get("tip_type")),
raw_value_fn=lambda value: TipType[value.upper()],
options=[x.name.lower() for x in TipType],
entity_category=EntityCategory.CONFIG,
),
)
async def async_setup_entry(
@ -157,11 +182,17 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up select entities from a config entry."""
coordinator = entry.runtime_data
coordinators = entry.runtime_data
descriptions = PINECIL_SELECT_DESCRIPTIONS
descriptions += (
PINECIL_SELECT_DESCRIPTIONS_V223
if coordinators.live_data.v223_features
else PINECIL_SELECT_DESCRIPTIONS_V222
)
async_add_entities(
IronOSSelectEntity(coordinator, description)
for description in PINECIL_SELECT_DESCRIPTIONS
IronOSSelectEntity(coordinators, description) for description in descriptions
)

View File

@ -94,6 +94,9 @@
},
"temp_increment_long": {
"name": "Long-press temperature step"
},
"hall_effect_sleep_time": {
"name": "Hall sensor sleep timeout"
}
},
"select": {
@ -173,6 +176,15 @@
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
}
},
"tip_type": {
"name": "Soldering tip type",
"state": {
"auto": "Auto sense",
"ts100_long": "TS100 long/Hakko T12 tip",
"pine_short": "Pinecil short tip",
"pts200": "PTS200 short tip"
}
}
},
"sensor": {
@ -223,7 +235,16 @@
"sleeping": "Sleeping",
"settings": "Settings",
"debug": "Debug",
"boost": "Boost"
"boost": "Boost",
"soldering_profile": "Soldering profile",
"temperature_adjust": "Temperature adjust",
"usb_pd_debug": "USB PD debug",
"thermal_runaway": "Thermal runaway",
"startup_logo": "Booting",
"cjc_calibration": "CJC calibration",
"startup_warnings": "Startup warnings",
"initialisation_done": "Initialisation done",
"hibernating": "Hibernating"
}
},
"estimated_power": {

View File

@ -20,6 +20,7 @@ from pynecil import (
ScrollSpeed,
SettingsDataResponse,
TempUnit,
TipType,
)
import pytest
@ -164,7 +165,7 @@ def mock_pynecil() -> Generator[AsyncMock]:
client = mock_client.return_value
client.get_device_info.return_value = DeviceInfoResponse(
build="v2.22",
build="v2.23",
device_id="c0ffeeC0",
address="c0:ff:ee:c0:ff:ee",
device_sn="0000c0ffeec0ffee",
@ -205,6 +206,8 @@ def mock_pynecil() -> Generator[AsyncMock]:
display_invert=True,
calibrate_cjc=True,
usb_pd_mode=True,
hall_sleep_time=5,
tip_type=TipType.PINE_SHORT,
)
client.get_live_data.return_value = LiveDataResponse(
live_temp=298,

View File

@ -6,7 +6,7 @@
}),
'device_info': dict({
'__type': "<class 'pynecil.types.DeviceInfoResponse'>",
'repr': "DeviceInfoResponse(build='v2.22', device_id='c0ffeeC0', address='c0:ff:ee:c0:ff:ee', device_sn='0000c0ffeec0ffee', name='Pinecil-C0FFEEE', is_synced=False)",
'repr': "DeviceInfoResponse(build='v2.23', device_id='c0ffeeC0', address='c0:ff:ee:c0:ff:ee', device_sn='0000c0ffeec0ffee', name='Pinecil-C0FFEEE', is_synced=False)",
}),
'live_data': dict({
'__type': "<class 'pynecil.types.LiveDataResponse'>",

View File

@ -226,6 +226,63 @@
'state': '7',
})
# ---
# name: test_state[number.pinecil_hall_sensor_sleep_timeout-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 60,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.pinecil_hall_sensor_sleep_timeout',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Hall sensor sleep timeout',
'platform': 'iron_os',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <PinecilNumber.HALL_EFFECT_SLEEP_TIME: 'hall_effect_sleep_time'>,
'unique_id': 'c0:ff:ee:c0:ff:ee_hall_effect_sleep_time',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_state[number.pinecil_hall_sensor_sleep_timeout-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pinecil Hall sensor sleep timeout',
'max': 60,
'min': 0,
'mode': <NumberMode.BOX: 'box'>,
'step': 5,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'number.pinecil_hall_sensor_sleep_timeout',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '5',
})
# ---
# name: test_state[number.pinecil_keep_awake_pulse_delay-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -250,6 +250,7 @@
'options': list([
'off',
'on',
'safe',
]),
}),
'config_entry_id': <ANY>,
@ -287,6 +288,7 @@
'options': list([
'off',
'on',
'safe',
]),
}),
'context': <ANY>,
@ -415,6 +417,66 @@
'state': 'fast',
})
# ---
# name: test_state[select.pinecil_soldering_tip_type-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'auto',
'ts100_long',
'pine_short',
'pts200',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.pinecil_soldering_tip_type',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Soldering tip type',
'platform': 'iron_os',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <PinecilSelect.TIP_TYPE: 'tip_type'>,
'unique_id': 'c0:ff:ee:c0:ff:ee_tip_type',
'unit_of_measurement': None,
})
# ---
# name: test_state[select.pinecil_soldering_tip_type-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Pinecil Soldering tip type',
'options': list([
'auto',
'ts100_long',
'pine_short',
'pts200',
]),
}),
'context': <ANY>,
'entity_id': 'select.pinecil_soldering_tip_type',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'pine_short',
})
# ---
# name: test_state[select.pinecil_start_up_behavior-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -45,7 +45,7 @@
'entity_picture': 'https://brands.home-assistant.io/_/iron_os/icon.png',
'friendly_name': 'Pinecil Firmware',
'in_progress': False,
'installed_version': 'v2.22',
'installed_version': 'v2.23',
'latest_version': 'v2.22',
'release_summary': None,
'release_url': 'https://github.com/Ralim/IronOS/releases/tag/v2.22',

View File

@ -4,13 +4,15 @@ from datetime import timedelta
from unittest.mock import AsyncMock
from freezegun.api import FrozenDateTimeFactory
from pynecil import CommunicationError
from pynecil import CommunicationError, DeviceInfoResponse
import pytest
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from .conftest import DEFAULT_NAME
from tests.common import MockConfigEntry, async_fire_time_changed
@ -89,3 +91,35 @@ async def test_settings_exception(
assert (state := hass.states.get("number.pinecil_boost_temperature"))
assert state.state == STATE_UNKNOWN
@pytest.mark.usefixtures(
"entity_registry_enabled_by_default", "mock_pynecil", "ble_device"
)
async def test_v223_entities_not_loaded(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_pynecil: AsyncMock,
) -> None:
"""Test the new entities in IronOS v2.23 are not loaded on smaller versions."""
mock_pynecil.get_device_info.return_value = DeviceInfoResponse(
build="v2.22",
device_id="c0ffeeC0",
address="c0:ff:ee:c0:ff:ee",
device_sn="0000c0ffeec0ffee",
name=DEFAULT_NAME,
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert hass.states.get("number.pinecil_hall_sensor_sleep_timeout") is None
assert hass.states.get("select.pinecil_soldering_tip_type") is None
assert (
state := hass.states.get("select.pinecil_power_delivery_3_1_epr")
) is not None
assert len(state.attributes["options"]) == 2

View File

@ -138,6 +138,12 @@ async def test_state(
("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),
(
"number.pinecil_hall_sensor_sleep_timeout",
CharSetting.HALL_SLEEP_TIME,
60,
60,
),
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device")

View File

@ -16,6 +16,7 @@ from pynecil import (
ScreenOrientationMode,
ScrollSpeed,
TempUnit,
TipType,
USBPDMode,
)
import pytest
@ -111,6 +112,11 @@ async def test_state(
"on",
(CharSetting.USB_PD_MODE, USBPDMode.ON),
),
(
"select.pinecil_soldering_tip_type",
"auto",
(CharSetting.TIP_TYPE, TipType.AUTO),
),
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ble_device")