Skip to content

Commit

Permalink
Add binary platform and tip connected detection to IronOS
Browse files Browse the repository at this point in the history
  • Loading branch information
tr4nt0r committed Dec 2, 2024
1 parent 1cf00d9 commit e5ed363
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 18 deletions.
7 changes: 6 additions & 1 deletion homeassistant/components/iron_os/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
IronOSSettingsCoordinator,
)

PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.UPDATE]
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.NUMBER,
Platform.SENSOR,
Platform.UPDATE,
]


CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
Expand Down
53 changes: 53 additions & 0 deletions homeassistant/components/iron_os/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Binary sensor platform for IronOS integration."""

from __future__ import annotations

from enum import StrEnum

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

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


class PinecilBinarySensor(StrEnum):
"""Pinecil Binary Sensors."""

TIP_CONNECTED = "tip_connected"


async def async_setup_entry(
hass: HomeAssistant,
entry: IronOSConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up binary sensors from a config entry."""
coordinator = entry.runtime_data

async_add_entities([IronOSBinarySensorEntity(coordinator)])


class IronOSBinarySensorEntity(IronOSBaseEntity, BinarySensorEntity):
"""Representation of a IronOS binary sensor entity."""

entity_description = BinarySensorEntityDescription(
key=PinecilBinarySensor.TIP_CONNECTED,
translation_key=PinecilBinarySensor.TIP_CONNECTED,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
)

def __init__(self, coordinator: IronOSLiveDataCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator, self.entity_description)

@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
return self.coordinator.has_tip
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 @@ async def _async_update_data(self) -> LiveDataResponse:
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
class IronOSFirmwareUpdateCoordinator(DataUpdateCoordinator[GitHubReleaseModel]):
"""IronOS coordinator for retrieving update information from github."""
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/iron_os/icons.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
{
"entity": {
"binary_sensor": {
"tip_connected": {
"default": "mdi:pencil-outline",
"state": {
"off": "mdi:pencil-off-outline"
}
}
},
"number": {
"setpoint_temperature": {
"default": "mdi:thermometer"
Expand Down
34 changes: 19 additions & 15 deletions homeassistant/components/iron_os/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,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 +64,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 +81,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 +90,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 +117,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 +126,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 +144,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 +161,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 +172,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 @@ -197,4 +199,6 @@ class IronOSSensorEntity(IronOSBaseEntity, SensorEntity):
@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
)
5 changes: 5 additions & 0 deletions homeassistant/components/iron_os/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
}
},
"entity": {
"binary_sensor": {
"tip_connected": {
"name": "Soldering tip"
}
},
"number": {
"setpoint_temperature": {
"name": "Setpoint temperature"
Expand Down
48 changes: 48 additions & 0 deletions tests/components/iron_os/snapshots/test_binary_sensor.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# serializer version: 1
# name: test_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_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',
})
# ---
74 changes: 74 additions & 0 deletions tests/components/iron_os/test_binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Tests for the Pinecil Binary Sensors."""

from collections.abc import AsyncGenerator
from datetime import timedelta
from unittest.mock import AsyncMock, patch

from pynecil import LiveDataResponse
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.components.binary_sensor import STATE_OFF, STATE_ON
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.util.dt as dt_util

from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform


@pytest.fixture(autouse=True)
async def binary_sensor_only() -> AsyncGenerator[None]:
"""Enable only the binary sensor platform."""
with patch(
"homeassistant.components.iron_os.PLATFORMS",
[Platform.BINARY_SENSOR],
):
yield


@pytest.mark.usefixtures(
"entity_registry_enabled_by_default", "mock_pynecil", "ble_device"
)
async def test_sensors(
hass: HomeAssistant,
config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test the Pinecil binary sensor platform."""
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

await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)


@pytest.mark.usefixtures(
"entity_registry_enabled_by_default", "ble_device", "mock_pynecil"
)
async def test_tip_on_off(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_pynecil: AsyncMock,
) -> None:
"""Test tip_connected binary sensor on/off states."""

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("binary_sensor.pinecil_soldering_tip").state == STATE_ON

mock_pynecil.get_live_data.return_value = LiveDataResponse(
live_temp=479,
max_tip_temp_ability=460,
)
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5))

assert hass.states.get("binary_sensor.pinecil_soldering_tip").state == STATE_OFF
Loading

0 comments on commit e5ed363

Please sign in to comment.