Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tip connected detection to IronOS #131946

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions homeassistant/components/iron_os/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@
except CommunicationError as e:
raise UpdateFailed("Cannot connect to device") from e

@property
def has_tip(self) -> bool:
"""Return True if the tip is connected."""
if (
self.data.max_tip_temp_ability is not None
and self.data.live_temp is not None
):
threshold = self.data.max_tip_temp_ability - 5
return self.data.live_temp <= threshold
return False

Check warning on line 104 in homeassistant/components/iron_os/coordinator.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/iron_os/coordinator.py#L104

Added line #L104 was not covered by tests


class IronOSFirmwareUpdateCoordinator(DataUpdateCoordinator[GitHubReleaseModel]):
"""IronOS coordinator for retrieving update information from github."""
Expand Down
36 changes: 21 additions & 15 deletions homeassistant/components/iron_os/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

from . import IronOSConfigEntry
from .const import OHM
from .coordinator import IronOSLiveDataCoordinator
from .entity import IronOSBaseEntity


Expand All @@ -54,7 +55,7 @@ class PinecilSensor(StrEnum):
class IronOSSensorEntityDescription(SensorEntityDescription):
"""IronOS sensor entity descriptions."""

value_fn: Callable[[LiveDataResponse], StateType]
value_fn: Callable[[LiveDataResponse, bool], StateType]


PINECIL_SENSOR_DESCRIPTIONS: tuple[IronOSSensorEntityDescription, ...] = (
Expand All @@ -64,15 +65,15 @@ class IronOSSensorEntityDescription(SensorEntityDescription):
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.live_temp,
value_fn=lambda data, has_tip: data.live_temp if has_tip else None,
),
IronOSSensorEntityDescription(
key=PinecilSensor.DC_VOLTAGE,
translation_key=PinecilSensor.DC_VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.dc_voltage,
value_fn=lambda data, _: data.dc_voltage,
entity_category=EntityCategory.DIAGNOSTIC,
),
IronOSSensorEntityDescription(
Expand All @@ -81,7 +82,7 @@ class IronOSSensorEntityDescription(SensorEntityDescription):
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.handle_temp,
value_fn=lambda data, _: data.handle_temp,
),
IronOSSensorEntityDescription(
key=PinecilSensor.PWMLEVEL,
Expand All @@ -90,22 +91,24 @@ class IronOSSensorEntityDescription(SensorEntityDescription):
suggested_display_precision=0,
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.pwm_level,
value_fn=lambda data, _: data.pwm_level,
entity_category=EntityCategory.DIAGNOSTIC,
),
IronOSSensorEntityDescription(
key=PinecilSensor.POWER_SRC,
translation_key=PinecilSensor.POWER_SRC,
device_class=SensorDeviceClass.ENUM,
options=[item.name.lower() for item in PowerSource],
value_fn=lambda data: data.power_src.name.lower() if data.power_src else None,
value_fn=(
lambda data, _: data.power_src.name.lower() if data.power_src else None
),
entity_category=EntityCategory.DIAGNOSTIC,
),
IronOSSensorEntityDescription(
key=PinecilSensor.TIP_RESISTANCE,
translation_key=PinecilSensor.TIP_RESISTANCE,
native_unit_of_measurement=OHM,
value_fn=lambda data: data.tip_resistance,
value_fn=lambda data, has_tip: data.tip_resistance if has_tip else None,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
),
Expand All @@ -115,7 +118,7 @@ class IronOSSensorEntityDescription(SensorEntityDescription):
native_unit_of_measurement=UnitOfTime.SECONDS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda data: data.uptime,
value_fn=lambda data, _: data.uptime,
entity_category=EntityCategory.DIAGNOSTIC,
),
IronOSSensorEntityDescription(
Expand All @@ -124,15 +127,15 @@ class IronOSSensorEntityDescription(SensorEntityDescription):
native_unit_of_measurement=UnitOfTime.SECONDS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.movement_time,
value_fn=lambda data, _: data.movement_time,
entity_category=EntityCategory.DIAGNOSTIC,
),
IronOSSensorEntityDescription(
key=PinecilSensor.MAX_TIP_TEMP_ABILITY,
translation_key=PinecilSensor.MAX_TIP_TEMP_ABILITY,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
value_fn=lambda data: data.max_tip_temp_ability,
value_fn=lambda data, has_tip: data.max_tip_temp_ability if has_tip else None,
entity_category=EntityCategory.DIAGNOSTIC,
),
IronOSSensorEntityDescription(
Expand All @@ -142,15 +145,15 @@ class IronOSSensorEntityDescription(SensorEntityDescription):
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
value_fn=lambda data: data.tip_voltage,
value_fn=lambda data, has_tip: data.tip_voltage if has_tip else None,
entity_category=EntityCategory.DIAGNOSTIC,
),
IronOSSensorEntityDescription(
key=PinecilSensor.HALL_SENSOR,
translation_key=PinecilSensor.HALL_SENSOR,
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
value_fn=lambda data: data.hall_sensor,
value_fn=lambda data, _: data.hall_sensor,
entity_category=EntityCategory.DIAGNOSTIC,
),
IronOSSensorEntityDescription(
Expand All @@ -159,7 +162,7 @@ class IronOSSensorEntityDescription(SensorEntityDescription):
device_class=SensorDeviceClass.ENUM,
options=[item.name.lower() for item in OperatingMode],
value_fn=(
lambda data: data.operating_mode.name.lower()
lambda data, _: data.operating_mode.name.lower()
if data.operating_mode
else None
),
Expand All @@ -170,7 +173,7 @@ class IronOSSensorEntityDescription(SensorEntityDescription):
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.estimated_power,
value_fn=lambda data, _: data.estimated_power,
),
)

Expand All @@ -193,8 +196,11 @@ class IronOSSensorEntity(IronOSBaseEntity, SensorEntity):
"""Representation of a IronOS sensor entity."""

entity_description: IronOSSensorEntityDescription
coordinator: IronOSLiveDataCoordinator

@property
def native_value(self) -> StateType:
"""Return sensor state."""
return self.entity_description.value_fn(self.coordinator.data)
return self.entity_description.value_fn(
self.coordinator.data, self.coordinator.has_tip
)
48 changes: 48 additions & 0 deletions tests/components/iron_os/snapshots/test_binary_sensor.ambr
tr4nt0r marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# serializer version: 1
# name: test_binary_sensors[binary_sensor.pinecil_soldering_tip-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.pinecil_soldering_tip',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
'original_icon': None,
'original_name': 'Soldering tip',
'platform': 'iron_os',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <PinecilBinarySensor.TIP_CONNECTED: 'tip_connected'>,
'unique_id': 'c0:ff:ee:c0:ff:ee_tip_connected',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[binary_sensor.pinecil_soldering_tip-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'connectivity',
'friendly_name': 'Pinecil Soldering tip',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.pinecil_soldering_tip',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
35 changes: 33 additions & 2 deletions tests/components/iron_os/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
from unittest.mock import AsyncMock, MagicMock, patch

from freezegun.api import FrozenDateTimeFactory
from pynecil import CommunicationError
from pynecil import CommunicationError, LiveDataResponse
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.iron_os.coordinator import SCAN_INTERVAL
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er

Expand Down Expand Up @@ -71,3 +71,34 @@ async def test_sensors_unavailable(
)
for entity_entry in entity_entries:
assert hass.states.get(entity_entry.entity_id).state == STATE_UNAVAILABLE


@pytest.mark.usefixtures(
"entity_registry_enabled_by_default", "ble_device", "mock_pynecil"
)
async def test_tip_detection(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_pynecil: AsyncMock,
ble_device: MagicMock,
) -> None:
"""Test sensor state is unknown when tip is disconnected."""

mock_pynecil.get_live_data.return_value = LiveDataResponse(
live_temp=479,
max_tip_temp_ability=460,
)

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
entities = {
"sensor.pinecil_tip_temperature",
"sensor.pinecil_max_tip_temperature",
"sensor.pinecil_raw_tip_voltage",
"sensor.pinecil_tip_resistance",
}
for entity_id in entities:
assert hass.states.get(entity_id).state == STATE_UNKNOWN