diff --git a/CODEOWNERS b/CODEOWNERS index fc18be91239..bf93676f962 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/iron_os/__init__.py b/homeassistant/components/iron_os/__init__.py new file mode 100644 index 00000000000..bf3c6c34c83 --- /dev/null +++ b/homeassistant/components/iron_os/__init__.py @@ -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) diff --git a/homeassistant/components/iron_os/config_flow.py b/homeassistant/components/iron_os/config_flow.py new file mode 100644 index 00000000000..444db79c926 --- /dev/null +++ b/homeassistant/components/iron_os/config_flow.py @@ -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)} + ), + ) diff --git a/homeassistant/components/iron_os/const.py b/homeassistant/components/iron_os/const.py new file mode 100644 index 00000000000..86b7d401f4f --- /dev/null +++ b/homeassistant/components/iron_os/const.py @@ -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" diff --git a/homeassistant/components/iron_os/coordinator.py b/homeassistant/components/iron_os/coordinator.py new file mode 100644 index 00000000000..e8424478d86 --- /dev/null +++ b/homeassistant/components/iron_os/coordinator.py @@ -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() diff --git a/homeassistant/components/iron_os/entity.py b/homeassistant/components/iron_os/entity.py new file mode 100644 index 00000000000..5a24b0a5567 --- /dev/null +++ b/homeassistant/components/iron_os/entity.py @@ -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})", + ) diff --git a/homeassistant/components/iron_os/icons.json b/homeassistant/components/iron_os/icons.json new file mode 100644 index 00000000000..0d207607a4f --- /dev/null +++ b/homeassistant/components/iron_os/icons.json @@ -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" + } + } + } +} diff --git a/homeassistant/components/iron_os/manifest.json b/homeassistant/components/iron_os/manifest.json new file mode 100644 index 00000000000..cfaf36880f2 --- /dev/null +++ b/homeassistant/components/iron_os/manifest.json @@ -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"] +} diff --git a/homeassistant/components/iron_os/sensor.py b/homeassistant/components/iron_os/sensor.py new file mode 100644 index 00000000000..095ffd254df --- /dev/null +++ b/homeassistant/components/iron_os/sensor.py @@ -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) diff --git a/homeassistant/components/iron_os/strings.json b/homeassistant/components/iron_os/strings.json new file mode 100644 index 00000000000..cb95330b768 --- /dev/null +++ b/homeassistant/components/iron_os/strings.json @@ -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" + } + } +} diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index cda011d1bef..2ea604a91a2 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -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", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e7d5278dd89..d350a58f3c6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -276,6 +276,7 @@ FLOWS = { "ipma", "ipp", "iqvia", + "iron_os", "islamic_prayer_times", "israel_rail", "iss", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 8bfef6a9887..8b0225ed063 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -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, diff --git a/requirements_all.txt b/requirements_all.txt index 3b599b00ce8..90ca4049d85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27d112fb4f4..7bdd1a910ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/iron_os/__init__.py b/tests/components/iron_os/__init__.py new file mode 100644 index 00000000000..4e27f2c741c --- /dev/null +++ b/tests/components/iron_os/__init__.py @@ -0,0 +1 @@ +"""Tests for the Pinecil integration.""" diff --git a/tests/components/iron_os/conftest.py b/tests/components/iron_os/conftest.py new file mode 100644 index 00000000000..b6983074441 --- /dev/null +++ b/tests/components/iron_os/conftest.py @@ -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 diff --git a/tests/components/iron_os/snapshots/test_sensor.ambr b/tests/components/iron_os/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..64cb951dacc --- /dev/null +++ b/tests/components/iron_os/snapshots/test_sensor.ambr @@ -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': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_dc_input_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'DC input voltage', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_dc_input_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Pinecil DC input voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_dc_input_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20.6', + }) +# --- +# name: test_sensors[sensor.pinecil_estimated_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pinecil_estimated_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Estimated power', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_estimated_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_estimated_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Pinecil Estimated power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_estimated_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '24.8', + }) +# --- +# name: test_sensors[sensor.pinecil_hall_effect_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_hall_effect_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + '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': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_hall_effect_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensors[sensor.pinecil_handle_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pinecil_handle_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Handle temperature', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_handle_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_handle_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pinecil Handle temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_handle_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '36.3', + }) +# --- +# name: test_sensors[sensor.pinecil_last_movement_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_last_movement_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Last movement time', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_movement_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_last_movement_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Pinecil Last movement time', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_last_movement_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10000', + }) +# --- +# name: test_sensors[sensor.pinecil_max_tip_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_max_tip_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Max tip temperature', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_max_tip_temp_ability', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_max_tip_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pinecil Max tip temperature', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_max_tip_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pinecil_operating_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Operating mode', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + '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': , + 'entity_id': 'sensor.pinecil_operating_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'soldering', + }) +# --- +# name: test_sensors[sensor.pinecil_power_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_power_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power level', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + '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': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.pinecil_power_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_power_source', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power source', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + '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': , + 'entity_id': 'sensor.pinecil_power_source', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'pd', + }) +# --- +# name: test_sensors[sensor.pinecil_raw_tip_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_raw_tip_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 3, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Raw tip voltage', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_tip_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_raw_tip_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Pinecil Raw tip voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_raw_tip_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2212', + }) +# --- +# name: test_sensors[sensor.pinecil_tip_resistance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_tip_resistance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + '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': , + 'entity_id': 'sensor.pinecil_tip_resistance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6.2', + }) +# --- +# name: test_sensors[sensor.pinecil_tip_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.pinecil_tip_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tip temperature', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_live_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_tip_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Pinecil Tip temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_tip_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '298', + }) +# --- +# name: test_sensors[sensor.pinecil_uptime-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.pinecil_uptime', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Uptime', + 'platform': 'iron_os', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'c0:ff:ee:c0:ff:ee_uptime', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.pinecil_uptime-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Pinecil Uptime', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.pinecil_uptime', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1671', + }) +# --- diff --git a/tests/components/iron_os/test_config_flow.py b/tests/components/iron_os/test_config_flow.py new file mode 100644 index 00000000000..231ec6cc3d6 --- /dev/null +++ b/tests/components/iron_os/test_config_flow.py @@ -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" diff --git a/tests/components/iron_os/test_sensor.py b/tests/components/iron_os/test_sensor.py new file mode 100644 index 00000000000..0c35193e400 --- /dev/null +++ b/tests/components/iron_os/test_sensor.py @@ -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