Integration for IronOS (Pinecil V2) soldering irons (#120802)
* Add Pinecil integration * Refactor with new library * Add tests for config flow, remove unused code * requested changes * update requirements * Move some sensor values to diagnostics, add tests for sensors * User service uuid in discovery * fix manufacturer name * Bump pynecil to version 0.2.0 * Rename integration to IronOS * Recreate snapshot * Update strings * type checking * Update snapshot * Add async_setup to coordinator * Show device id with serial number * Added missing boost to operation mode states * remove super call * Refactor * testspull/122765/head
parent
06ee8fdd47
commit
70df4ca461
|
@ -710,6 +710,8 @@ build.json @home-assistant/supervisor
|
|||
/homeassistant/components/iqvia/ @bachya
|
||||
/tests/components/iqvia/ @bachya
|
||||
/homeassistant/components/irish_rail_transport/ @ttroy50
|
||||
/homeassistant/components/iron_os/ @tr4nt0r
|
||||
/tests/components/iron_os/ @tr4nt0r
|
||||
/homeassistant/components/isal/ @bdraco
|
||||
/tests/components/isal/ @bdraco
|
||||
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
"""The IronOS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pynecil import Pynecil
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import IronOSCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
type IronOSConfigEntry = ConfigEntry[IronOSCoordinator]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: IronOSConfigEntry) -> bool:
|
||||
"""Set up IronOS from a config entry."""
|
||||
if TYPE_CHECKING:
|
||||
assert entry.unique_id
|
||||
ble_device = bluetooth.async_ble_device_from_address(
|
||||
hass, entry.unique_id, connectable=True
|
||||
)
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_device_unavailable_exception",
|
||||
translation_placeholders={CONF_NAME: entry.title},
|
||||
)
|
||||
|
||||
device = Pynecil(ble_device)
|
||||
|
||||
coordinator = IronOSCoordinator(hass, device)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: IronOSConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
@ -0,0 +1,83 @@
|
|||
"""Config flow for IronOS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from habluetooth import BluetoothServiceInfoBleak
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bluetooth.api import async_discovered_service_info
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ADDRESS
|
||||
|
||||
from .const import DISCOVERY_SVC_UUID, DOMAIN
|
||||
|
||||
|
||||
class IronOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for IronOS."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._discovery_info: BluetoothServiceInfoBleak | None = None
|
||||
self._discovered_devices: dict[str, str] = {}
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle bluetooth discovery step."""
|
||||
await self.async_set_unique_id(discovery_info.address)
|
||||
self._abort_if_unique_id_configured()
|
||||
self._discovery_info = discovery_info
|
||||
|
||||
return await self.async_step_bluetooth_confirm()
|
||||
|
||||
async def async_step_bluetooth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm discovery."""
|
||||
assert self._discovery_info is not None
|
||||
discovery_info = self._discovery_info
|
||||
title = discovery_info.name
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title=title, data={})
|
||||
|
||||
self._set_confirm_only()
|
||||
placeholders = {"name": title}
|
||||
self.context["title_placeholders"] = placeholders
|
||||
return self.async_show_form(
|
||||
step_id="bluetooth_confirm", description_placeholders=placeholders
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the user step to pick discovered device."""
|
||||
if user_input is not None:
|
||||
address = user_input[CONF_ADDRESS]
|
||||
title = self._discovered_devices[address]
|
||||
await self.async_set_unique_id(address, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=title, data={})
|
||||
|
||||
current_addresses = self._async_current_ids()
|
||||
for discovery_info in async_discovered_service_info(self.hass, True):
|
||||
address = discovery_info.address
|
||||
if (
|
||||
DISCOVERY_SVC_UUID not in discovery_info.service_uuids
|
||||
or address in current_addresses
|
||||
or address in self._discovered_devices
|
||||
):
|
||||
continue
|
||||
self._discovered_devices[address] = discovery_info.name
|
||||
|
||||
if not self._discovered_devices:
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)}
|
||||
),
|
||||
)
|
|
@ -0,0 +1,10 @@
|
|||
"""Constants for the IronOS integration."""
|
||||
|
||||
DOMAIN = "iron_os"
|
||||
|
||||
MANUFACTURER = "PINE64"
|
||||
MODEL = "Pinecil V2"
|
||||
|
||||
OHM = "Ω"
|
||||
|
||||
DISCOVERY_SVC_UUID = "9eae1000-9d0d-48c5-aa55-33e27f9bc533"
|
|
@ -0,0 +1,49 @@
|
|||
"""Update coordinator for IronOS Integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pynecil import CommunicationError, DeviceInfoResponse, LiveDataResponse, Pynecil
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
|
||||
class IronOSCoordinator(DataUpdateCoordinator[LiveDataResponse]):
|
||||
"""IronOS coordinator."""
|
||||
|
||||
device_info: DeviceInfoResponse
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, device: Pynecil) -> None:
|
||||
"""Initialize IronOS coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.device = device
|
||||
|
||||
async def _async_update_data(self) -> LiveDataResponse:
|
||||
"""Fetch data from Device."""
|
||||
|
||||
try:
|
||||
return await self.device.get_live_data()
|
||||
|
||||
except CommunicationError as e:
|
||||
raise UpdateFailed("Cannot connect to device") from e
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
|
||||
self.device_info = await self.device.get_device_info()
|
|
@ -0,0 +1,41 @@
|
|||
"""Base entity for IronOS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
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 IronOSCoordinator
|
||||
|
||||
|
||||
class IronOSBaseEntity(CoordinatorEntity[IronOSCoordinator]):
|
||||
"""Base IronOS entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: IronOSCoordinator,
|
||||
entity_description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.config_entry.unique_id}_{entity_description.key}"
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
assert coordinator.config_entry.unique_id
|
||||
self.device_info = DeviceInfo(
|
||||
connections={(CONNECTION_BLUETOOTH, coordinator.config_entry.unique_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=MODEL,
|
||||
name="Pinecil",
|
||||
sw_version=coordinator.device_info.build,
|
||||
serial_number=f"{coordinator.device_info.device_sn} (ID:{coordinator.device_info.device_id})",
|
||||
)
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"live_temperature": {
|
||||
"default": "mdi:soldering-iron"
|
||||
},
|
||||
"setpoint_temperature": {
|
||||
"default": "mdi:thermostat"
|
||||
},
|
||||
"voltage": {
|
||||
"default": "mdi:current-dc"
|
||||
},
|
||||
"handle_temperature": {
|
||||
"default": "mdi:grease-pencil"
|
||||
},
|
||||
"power_pwm_level": {
|
||||
"default": "mdi:square-wave"
|
||||
},
|
||||
"power_source": {
|
||||
"default": "mdi:power-plug",
|
||||
"state": {
|
||||
"dc": "mdi:record-circle-outline",
|
||||
"qc": "mdi:usb-port",
|
||||
"pd_vbus": "mdi:usb-c-port",
|
||||
"pd": "mdi:usb-c-port"
|
||||
}
|
||||
},
|
||||
"tip_resistance": {
|
||||
"default": "mdi:omega"
|
||||
},
|
||||
"hall_sensor": {
|
||||
"default": "mdi:leak"
|
||||
},
|
||||
"movement_time": {
|
||||
"default": "mdi:clock-fast"
|
||||
},
|
||||
"max_tip_temp_ability": {
|
||||
"default": "mdi:thermometer-chevron-up"
|
||||
},
|
||||
"uptime": {
|
||||
"default": "mdi:progress-clock"
|
||||
},
|
||||
"tip_voltage": {
|
||||
"default": "mdi:sine-wave"
|
||||
},
|
||||
"operating_mode": {
|
||||
"default": "mdi:format-list-bulleted",
|
||||
"state": {
|
||||
"boost": "mdi:rocket-launch",
|
||||
"soldering": "mdi:soldering-iron",
|
||||
"sleeping": "mdi:sleep",
|
||||
"settings": "mdi:menu-open",
|
||||
"debug": "mdi:bug-play"
|
||||
}
|
||||
},
|
||||
"estimated_power": {
|
||||
"default": "mdi:flash"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"domain": "iron_os",
|
||||
"name": "IronOS",
|
||||
"bluetooth": [
|
||||
{
|
||||
"service_uuid": "9eae1000-9d0d-48c5-aa55-33e27f9bc533",
|
||||
"connectable": true
|
||||
}
|
||||
],
|
||||
"codeowners": ["@tr4nt0r"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/iron_os",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pynecil"],
|
||||
"requirements": ["pynecil==0.2.0"]
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
"""Sensor platform for IronOS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
from pynecil import LiveDataResponse, OperatingMode, PowerSource
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
EntityCategory,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import IronOSConfigEntry
|
||||
from .const import OHM
|
||||
from .entity import IronOSBaseEntity
|
||||
|
||||
|
||||
class PinecilSensor(StrEnum):
|
||||
"""Pinecil Sensors."""
|
||||
|
||||
LIVE_TEMP = "live_temperature"
|
||||
SETPOINT_TEMP = "setpoint_temperature"
|
||||
DC_VOLTAGE = "voltage"
|
||||
HANDLETEMP = "handle_temperature"
|
||||
PWMLEVEL = "power_pwm_level"
|
||||
POWER_SRC = "power_source"
|
||||
TIP_RESISTANCE = "tip_resistance"
|
||||
UPTIME = "uptime"
|
||||
MOVEMENT_TIME = "movement_time"
|
||||
MAX_TIP_TEMP_ABILITY = "max_tip_temp_ability"
|
||||
TIP_VOLTAGE = "tip_voltage"
|
||||
HALL_SENSOR = "hall_sensor"
|
||||
OPERATING_MODE = "operating_mode"
|
||||
ESTIMATED_POWER = "estimated_power"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class IronOSSensorEntityDescription(SensorEntityDescription):
|
||||
"""IronOS sensor entity descriptions."""
|
||||
|
||||
value_fn: Callable[[LiveDataResponse], StateType]
|
||||
|
||||
|
||||
PINECIL_SENSOR_DESCRIPTIONS: tuple[IronOSSensorEntityDescription, ...] = (
|
||||
IronOSSensorEntityDescription(
|
||||
key=PinecilSensor.LIVE_TEMP,
|
||||
translation_key=PinecilSensor.LIVE_TEMP,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.live_temp,
|
||||
),
|
||||
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,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
IronOSSensorEntityDescription(
|
||||
key=PinecilSensor.HANDLETEMP,
|
||||
translation_key=PinecilSensor.HANDLETEMP,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.handle_temp,
|
||||
),
|
||||
IronOSSensorEntityDescription(
|
||||
key=PinecilSensor.PWMLEVEL,
|
||||
translation_key=PinecilSensor.PWMLEVEL,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
suggested_display_precision=0,
|
||||
device_class=SensorDeviceClass.POWER_FACTOR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
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,
|
||||
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,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
IronOSSensorEntityDescription(
|
||||
key=PinecilSensor.UPTIME,
|
||||
translation_key=PinecilSensor.UPTIME,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda data: data.uptime,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
IronOSSensorEntityDescription(
|
||||
key=PinecilSensor.MOVEMENT_TIME,
|
||||
translation_key=PinecilSensor.MOVEMENT_TIME,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
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,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
IronOSSensorEntityDescription(
|
||||
key=PinecilSensor.TIP_VOLTAGE,
|
||||
translation_key=PinecilSensor.TIP_VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=3,
|
||||
value_fn=lambda data: data.tip_voltage,
|
||||
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,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
IronOSSensorEntityDescription(
|
||||
key=PinecilSensor.OPERATING_MODE,
|
||||
translation_key=PinecilSensor.OPERATING_MODE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[item.name.lower() for item in OperatingMode],
|
||||
value_fn=(
|
||||
lambda data: data.operating_mode.name.lower()
|
||||
if data.operating_mode
|
||||
else None
|
||||
),
|
||||
),
|
||||
IronOSSensorEntityDescription(
|
||||
key=PinecilSensor.ESTIMATED_POWER,
|
||||
translation_key=PinecilSensor.ESTIMATED_POWER,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.estimated_power,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: IronOSConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensors from a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
IronOSSensorEntity(coordinator, description)
|
||||
for description in PINECIL_SENSOR_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class IronOSSensorEntity(IronOSBaseEntity, SensorEntity):
|
||||
"""Representation of a IronOS sensor entity."""
|
||||
|
||||
entity_description: IronOSSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return sensor state."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "[%key:component::bluetooth::config::step::user::description%]",
|
||||
"data": {
|
||||
"address": "[%key:common::config_flow::data::device%]"
|
||||
}
|
||||
},
|
||||
"bluetooth_confirm": {
|
||||
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"live_temperature": {
|
||||
"name": "Tip temperature"
|
||||
},
|
||||
"voltage": {
|
||||
"name": "DC input voltage"
|
||||
},
|
||||
"handle_temperature": {
|
||||
"name": "Handle temperature"
|
||||
},
|
||||
"power_pwm_level": {
|
||||
"name": "Power level"
|
||||
},
|
||||
"power_source": {
|
||||
"name": "Power source",
|
||||
"state": {
|
||||
"dc": "DC input",
|
||||
"qc": "USB Quick Charge",
|
||||
"pd_vbus": "USB PD VBUS",
|
||||
"pd": "USB Power Delivery"
|
||||
}
|
||||
},
|
||||
"tip_resistance": {
|
||||
"name": "Tip resistance"
|
||||
},
|
||||
"uptime": {
|
||||
"name": "Uptime"
|
||||
},
|
||||
"movement_time": {
|
||||
"name": "Last movement time"
|
||||
},
|
||||
"max_tip_temp_ability": {
|
||||
"name": "Max tip temperature"
|
||||
},
|
||||
"tip_voltage": {
|
||||
"name": "Raw tip voltage"
|
||||
},
|
||||
"hall_sensor": {
|
||||
"name": "Hall effect strength"
|
||||
},
|
||||
"operating_mode": {
|
||||
"name": "Operating mode",
|
||||
"state": {
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"soldering": "Soldering",
|
||||
"sleeping": "Sleeping",
|
||||
"settings": "Settings",
|
||||
"debug": "Debug",
|
||||
"boost": "Boost"
|
||||
}
|
||||
},
|
||||
"estimated_power": {
|
||||
"name": "Estimated power"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"setup_device_unavailable_exception": {
|
||||
"message": "Device {name} is not reachable"
|
||||
},
|
||||
"setup_device_connection_error_exception": {
|
||||
"message": "Connection to device {name} failed, try again later"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -321,6 +321,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
|
|||
"domain": "inkbird",
|
||||
"local_name": "tps",
|
||||
},
|
||||
{
|
||||
"connectable": True,
|
||||
"domain": "iron_os",
|
||||
"service_uuid": "9eae1000-9d0d-48c5-aa55-33e27f9bc533",
|
||||
},
|
||||
{
|
||||
"connectable": False,
|
||||
"domain": "kegtron",
|
||||
|
|
|
@ -276,6 +276,7 @@ FLOWS = {
|
|||
"ipma",
|
||||
"ipp",
|
||||
"iqvia",
|
||||
"iron_os",
|
||||
"islamic_prayer_times",
|
||||
"israel_rail",
|
||||
"iss",
|
||||
|
|
|
@ -2899,6 +2899,12 @@
|
|||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"iron_os": {
|
||||
"name": "IronOS",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"islamic_prayer_times": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
|
|
|
@ -2025,6 +2025,9 @@ pymsteams==0.1.12
|
|||
# homeassistant.components.mysensors
|
||||
pymysensors==0.24.0
|
||||
|
||||
# homeassistant.components.iron_os
|
||||
pynecil==0.2.0
|
||||
|
||||
# homeassistant.components.netgear
|
||||
pynetgear==0.10.10
|
||||
|
||||
|
|
|
@ -1615,6 +1615,9 @@ pymonoprice==0.4
|
|||
# homeassistant.components.mysensors
|
||||
pymysensors==0.24.0
|
||||
|
||||
# homeassistant.components.iron_os
|
||||
pynecil==0.2.0
|
||||
|
||||
# homeassistant.components.netgear
|
||||
pynetgear==0.10.10
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Pinecil integration."""
|
|
@ -0,0 +1,141 @@
|
|||
"""Fixtures for Pinecil tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from bleak.backends.device import BLEDevice
|
||||
from habluetooth import BluetoothServiceInfoBleak
|
||||
from pynecil import DeviceInfoResponse, LiveDataResponse, OperatingMode, PowerSource
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.iron_os import DOMAIN
|
||||
from homeassistant.const import CONF_ADDRESS
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.bluetooth import generate_advertisement_data, generate_ble_device
|
||||
|
||||
USER_INPUT = {CONF_ADDRESS: "c0:ff:ee:c0:ff:ee"}
|
||||
DEFAULT_NAME = "Pinecil-C0FFEEE"
|
||||
PINECIL_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||
name="Pinecil-C0FFEEE",
|
||||
address="c0:ff:ee:c0:ff:ee",
|
||||
device=generate_ble_device(
|
||||
address="c0:ff:ee:c0:ff:ee",
|
||||
name="Pinecil-C0FFEEE",
|
||||
),
|
||||
rssi=-61,
|
||||
manufacturer_data={},
|
||||
service_data={},
|
||||
service_uuids=["9eae1000-9d0d-48c5-aa55-33e27f9bc533"],
|
||||
source="local",
|
||||
advertisement=generate_advertisement_data(
|
||||
manufacturer_data={},
|
||||
service_uuids=["9eae1000-9d0d-48c5-aa55-33e27f9bc533"],
|
||||
),
|
||||
connectable=True,
|
||||
time=0,
|
||||
tx_power=None,
|
||||
)
|
||||
|
||||
UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||
name="",
|
||||
address="c0:ff:ee:c0:ff:ee",
|
||||
device=generate_ble_device(
|
||||
address="c0:ff:ee:c0:ff:ee",
|
||||
name="",
|
||||
),
|
||||
rssi=-61,
|
||||
manufacturer_data={},
|
||||
service_data={},
|
||||
service_uuids=[],
|
||||
source="local",
|
||||
advertisement=generate_advertisement_data(
|
||||
manufacturer_data={},
|
||||
service_uuids=[],
|
||||
),
|
||||
connectable=True,
|
||||
time=0,
|
||||
tx_power=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_bluetooth(enable_bluetooth: None) -> None:
|
||||
"""Auto mock bluetooth."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.iron_os.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture(name="discovery")
|
||||
def mock_async_discovered_service_info() -> Generator[MagicMock]:
|
||||
"""Mock service discovery."""
|
||||
with patch(
|
||||
"homeassistant.components.iron_os.config_flow.async_discovered_service_info",
|
||||
return_value=[PINECIL_SERVICE_INFO, UNKNOWN_SERVICE_INFO],
|
||||
) as discovery:
|
||||
yield discovery
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock Pinecil configuration entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=DEFAULT_NAME,
|
||||
data={},
|
||||
unique_id="c0:ff:ee:c0:ff:ee",
|
||||
entry_id="1234567890",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="ble_device")
|
||||
def mock_ble_device() -> Generator[MagicMock]:
|
||||
"""Mock BLEDevice."""
|
||||
with patch(
|
||||
"homeassistant.components.bluetooth.async_ble_device_from_address",
|
||||
return_value=BLEDevice(
|
||||
address="c0:ff:ee:c0:ff:ee", name=DEFAULT_NAME, rssi=-50, details={}
|
||||
),
|
||||
) as ble_device:
|
||||
yield ble_device
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pynecil() -> Generator[AsyncMock, None, None]:
|
||||
"""Mock Pynecil library."""
|
||||
with patch(
|
||||
"homeassistant.components.iron_os.Pynecil", autospec=True
|
||||
) as mock_client:
|
||||
client = mock_client.return_value
|
||||
|
||||
client.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,
|
||||
)
|
||||
client.get_live_data.return_value = LiveDataResponse(
|
||||
live_temp=298,
|
||||
setpoint_temp=300,
|
||||
dc_voltage=20.6,
|
||||
handle_temp=36.3,
|
||||
pwm_level=41,
|
||||
power_src=PowerSource.PD,
|
||||
tip_resistance=6.2,
|
||||
uptime=1671,
|
||||
movement_time=10000,
|
||||
max_tip_temp_ability=460,
|
||||
tip_voltage=2212,
|
||||
hall_sensor=0,
|
||||
operating_mode=OperatingMode.SOLDERING,
|
||||
estimated_power=24.8,
|
||||
)
|
||||
yield client
|
|
@ -0,0 +1,683 @@
|
|||
# serializer version: 1
|
||||
# name: test_sensors[sensor.pinecil_dc_input_voltage-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.pinecil_dc_input_voltage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'DC input voltage',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.DC_VOLTAGE: 'voltage'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_voltage',
|
||||
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_dc_input_voltage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'voltage',
|
||||
'friendly_name': 'Pinecil DC input voltage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_dc_input_voltage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '20.6',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_estimated_power-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pinecil_estimated_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Estimated power',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.ESTIMATED_POWER: 'estimated_power'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_estimated_power',
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_estimated_power-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power',
|
||||
'friendly_name': 'Pinecil Estimated power',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_estimated_power',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '24.8',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_hall_effect_strength-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.pinecil_hall_effect_strength',
|
||||
'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 effect strength',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.HALL_SENSOR: 'hall_sensor'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_hall_sensor',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_hall_effect_strength-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Pinecil Hall effect strength',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_hall_effect_strength',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_handle_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pinecil_handle_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Handle temperature',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.HANDLETEMP: 'handle_temperature'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_handle_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_handle_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Pinecil Handle temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_handle_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '36.3',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_last_movement_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.pinecil_last_movement_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Last movement time',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.MOVEMENT_TIME: 'movement_time'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_movement_time',
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_last_movement_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'Pinecil Last movement time',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_last_movement_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '10000',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_max_tip_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.pinecil_max_tip_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Max tip temperature',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.MAX_TIP_TEMP_ABILITY: 'max_tip_temp_ability'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_max_tip_temp_ability',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_max_tip_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Pinecil Max tip temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_max_tip_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '460',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_operating_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'idle',
|
||||
'soldering',
|
||||
'boost',
|
||||
'sleeping',
|
||||
'settings',
|
||||
'debug',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pinecil_operating_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Operating mode',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.OPERATING_MODE: 'operating_mode'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_operating_mode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_operating_mode-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Pinecil Operating mode',
|
||||
'options': list([
|
||||
'idle',
|
||||
'soldering',
|
||||
'boost',
|
||||
'sleeping',
|
||||
'settings',
|
||||
'debug',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_operating_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'soldering',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_power_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.pinecil_power_level',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.POWER_FACTOR: 'power_factor'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power level',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.PWMLEVEL: 'power_pwm_level'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_power_pwm_level',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_power_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'power_factor',
|
||||
'friendly_name': 'Pinecil Power level',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_power_level',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '41',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_power_source-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'dc',
|
||||
'qc',
|
||||
'pd_vbus',
|
||||
'pd',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.pinecil_power_source',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Power source',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.POWER_SRC: 'power_source'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_power_source',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_power_source-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'friendly_name': 'Pinecil Power source',
|
||||
'options': list([
|
||||
'dc',
|
||||
'qc',
|
||||
'pd_vbus',
|
||||
'pd',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_power_source',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'pd',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_raw_tip_voltage-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.pinecil_raw_tip_voltage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 3,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Raw tip voltage',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.TIP_VOLTAGE: 'tip_voltage'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_tip_voltage',
|
||||
'unit_of_measurement': <UnitOfElectricPotential.MILLIVOLT: 'mV'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_raw_tip_voltage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'voltage',
|
||||
'friendly_name': 'Pinecil Raw tip voltage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfElectricPotential.MILLIVOLT: 'mV'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_raw_tip_voltage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2212',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_tip_resistance-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.pinecil_tip_resistance',
|
||||
'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': 'Tip resistance',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.TIP_RESISTANCE: 'tip_resistance'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_tip_resistance',
|
||||
'unit_of_measurement': 'Ω',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_tip_resistance-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Pinecil Tip resistance',
|
||||
'unit_of_measurement': 'Ω',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_tip_resistance',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '6.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_tip_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.pinecil_tip_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Tip temperature',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.LIVE_TEMP: 'live_temperature'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_live_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_tip_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'friendly_name': 'Pinecil Tip temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_tip_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '298',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_uptime-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.pinecil_uptime',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Uptime',
|
||||
'platform': 'iron_os',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <PinecilSensor.UPTIME: 'uptime'>,
|
||||
'unique_id': 'c0:ff:ee:c0:ff:ee_uptime',
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.pinecil_uptime-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'Pinecil Uptime',
|
||||
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
|
||||
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.pinecil_uptime',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1671',
|
||||
})
|
||||
# ---
|
|
@ -0,0 +1,66 @@
|
|||
"""Tests for the Pinecil config flow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from homeassistant.components.iron_os import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .conftest import DEFAULT_NAME, PINECIL_SERVICE_INFO, USER_INPUT
|
||||
|
||||
|
||||
async def test_form(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, discovery: MagicMock
|
||||
) -> None:
|
||||
"""Test the user config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == DEFAULT_NAME
|
||||
assert result["data"] == {}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_no_device_discovered(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
discovery: MagicMock,
|
||||
) -> None:
|
||||
"""Test setup with no device discoveries."""
|
||||
discovery.return_value = []
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
|
||||
|
||||
async def test_async_step_bluetooth(hass: HomeAssistant) -> None:
|
||||
"""Test discovery via bluetooth.."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=PINECIL_SERVICE_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "bluetooth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == DEFAULT_NAME
|
||||
assert result["data"] == {}
|
||||
assert result["result"].unique_id == "c0:ff:ee:c0:ff:ee"
|
|
@ -0,0 +1,73 @@
|
|||
"""Tests for the Pinecil Sensors."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pynecil import CommunicationError
|
||||
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.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def sensor_only() -> AsyncGenerator[None, None]:
|
||||
"""Enable only the sensor platform."""
|
||||
with patch(
|
||||
"homeassistant.components.iron_os.PLATFORMS",
|
||||
[Platform.SENSOR],
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_sensors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_pynecil: AsyncMock,
|
||||
ble_device: MagicMock,
|
||||
) -> None:
|
||||
"""Test the Pinecil 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")
|
||||
async def test_sensors_unavailable(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_pynecil: AsyncMock,
|
||||
ble_device: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test the sensors when device disconnects."""
|
||||
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
|
||||
|
||||
mock_pynecil.get_live_data.side_effect = CommunicationError
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
)
|
||||
for entity_entry in entity_entries:
|
||||
assert hass.states.get(entity_entry.entity_id).state == STATE_UNAVAILABLE
|
Loading…
Reference in New Issue