diff --git a/CODEOWNERS b/CODEOWNERS index 7dc825fde3b..9d9ff9544b4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1600,6 +1600,8 @@ build.json @home-assistant/supervisor /homeassistant/components/solaredge_local/ @drobtravels @scheric /homeassistant/components/solarlog/ @Ernst79 @dontinelli /tests/components/solarlog/ @Ernst79 @dontinelli +/homeassistant/components/solarman/ @solarmanpv +/tests/components/solarman/ @solarmanpv /homeassistant/components/solax/ @squishykid @Darsstar /tests/components/solax/ @squishykid @Darsstar /homeassistant/components/soma/ @ratsept diff --git a/homeassistant/components/solarman/__init__.py b/homeassistant/components/solarman/__init__.py new file mode 100644 index 00000000000..d9054ee8753 --- /dev/null +++ b/homeassistant/components/solarman/__init__.py @@ -0,0 +1,24 @@ +"""Home Assistant integration for SOLARMAN devices.""" + +from __future__ import annotations + +from homeassistant.core import HomeAssistant + +from .const import PLATFORMS +from .coordinator import SolarmanConfigEntry, SolarmanDeviceUpdateCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: SolarmanConfigEntry) -> bool: + """Set up Solarman from a config entry.""" + coordinator = SolarmanDeviceUpdateCoordinator(hass, entry) + + 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: SolarmanConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/solarman/config_flow.py b/homeassistant/components/solarman/config_flow.py new file mode 100644 index 00000000000..9f3978bb35e --- /dev/null +++ b/homeassistant/components/solarman/config_flow.py @@ -0,0 +1,152 @@ +"""Config flow for solarman integration.""" + +import logging +from typing import Any + +from solarman_opendata.solarman import Solarman +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_MODEL, CONF_TYPE +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo + +from .const import ( + CONF_PRODUCT_TYPE, + CONF_SERIAL, + CONF_SN, + DEFAULT_PORT, + DOMAIN, + MODEL_NAME_MAP, +) + +_LOGGER = logging.getLogger(__name__) + + +class SolarmanConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Solarman.""" + + VERSION = 1 + + host: str | None = None + model: str | None = None + device_sn: str | None = None + mac: str | None = None + client: Solarman | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step via user interface.""" + errors = {} + if user_input is not None: + self.host = user_input[CONF_HOST] + + self.client = Solarman( + async_get_clientsession(self.hass), self.host, DEFAULT_PORT + ) + + try: + config_data = await self.client.get_config() + except TimeoutError: + errors["base"] = "timeout" + except ConnectionError: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unknown error occurred while verifying device") + errors["base"] = "unknown" + else: + device_info = config_data.get(CONF_DEVICE, config_data) + + self.device_sn = device_info[CONF_SN] + self.model = device_info[CONF_TYPE] + self.mac = dr.format_mac(device_info[CONF_MAC]) + + await self.async_set_unique_id(self.device_sn) + self._abort_if_unique_id_configured() + + if not errors: + return self.async_create_entry( + title=f"{MODEL_NAME_MAP[self.model]} ({self.host})", + data={ + CONF_HOST: self.host, + CONF_SN: self.device_sn, + CONF_MODEL: self.model, + CONF_MAC: self.mac, + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + } + ), + errors=errors, + ) + + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> ConfigFlowResult: + """Handle zeroconf discovery.""" + self.host = discovery_info.host + self.model = discovery_info.properties[CONF_PRODUCT_TYPE] + self.device_sn = discovery_info.properties[CONF_SERIAL] + + self.client = Solarman( + async_get_clientsession(self.hass), self.host, DEFAULT_PORT + ) + + try: + config_data = await self.client.get_config() + except TimeoutError: + return self.async_abort(reason="timeout") + except ConnectionError: + return self.async_abort(reason="cannot_connect") + except Exception: + _LOGGER.exception("Unknown error occurred while verifying device") + return self.async_abort(reason="unknown") + + device_info = config_data.get(CONF_DEVICE, config_data) + self.mac = dr.format_mac(device_info[CONF_MAC]) + + await self.async_set_unique_id(self.device_sn) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host}) + + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm discovery.""" + assert self.host + assert self.model + assert self.device_sn + + if user_input is not None: + return self.async_create_entry( + title=f"{MODEL_NAME_MAP[self.model]} ({self.host})", + data={ + CONF_HOST: self.host, + CONF_SN: self.device_sn, + CONF_MODEL: self.model, + CONF_MAC: self.mac, + }, + ) + + self._set_confirm_only() + + name = f"{self.model} ({self.device_sn})" + self.context["title_placeholders"] = {"name": name} + + return self.async_show_form( + step_id="discovery_confirm", + description_placeholders={ + CONF_MODEL: self.model, + CONF_SN: self.device_sn, + CONF_HOST: self.host, + CONF_MAC: self.mac or "", + }, + ) diff --git a/homeassistant/components/solarman/const.py b/homeassistant/components/solarman/const.py new file mode 100644 index 00000000000..729ec9b0559 --- /dev/null +++ b/homeassistant/components/solarman/const.py @@ -0,0 +1,23 @@ +"""Constants for the solarman integration.""" + +from datetime import timedelta + +from homeassistant.const import Platform + +DOMAIN = "solarman" +DEFAULT_PORT = 8080 +UPDATE_INTERVAL = timedelta(seconds=30) +PLATFORMS = [ + Platform.SENSOR, +] + +CONF_SERIAL = "serial" +CONF_SN = "sn" +CONF_FW = "fw" +CONF_PRODUCT_TYPE = "product_type" + +MODEL_NAME_MAP = { + "SP-2W-EU": "Smart Plug", + "P1-2W": "P1 Meter Reader", + "gl meter": "Smart Meter", +} diff --git a/homeassistant/components/solarman/coordinator.py b/homeassistant/components/solarman/coordinator.py new file mode 100644 index 00000000000..77dbbd80e45 --- /dev/null +++ b/homeassistant/components/solarman/coordinator.py @@ -0,0 +1,51 @@ +"""Coordinator for solarman integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from solarman_opendata.solarman import Solarman + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_PORT, DOMAIN, UPDATE_INTERVAL + +_LOGGER = logging.getLogger(__name__) + +type SolarmanConfigEntry = ConfigEntry[SolarmanDeviceUpdateCoordinator] + + +class SolarmanDeviceUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Coordinator for managing Solarman device data updates and control operations.""" + + config_entry: SolarmanConfigEntry + + def __init__(self, hass: HomeAssistant, config_entry: SolarmanConfigEntry) -> None: + """Initialize the Solarman device coordinator.""" + + super().__init__( + hass, + logger=_LOGGER, + config_entry=config_entry, + name=DOMAIN, + update_interval=UPDATE_INTERVAL, + ) + + self.api = Solarman( + async_get_clientsession(hass), config_entry.data[CONF_HOST], DEFAULT_PORT + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Fetch and update device data.""" + try: + return await self.api.fetch_data() + except ConnectionError as e: + raise UpdateFailed( + translation_domain=DOMAIN, + translation_key="update_failed", + ) from e diff --git a/homeassistant/components/solarman/entity.py b/homeassistant/components/solarman/entity.py new file mode 100644 index 00000000000..0a0920a75b1 --- /dev/null +++ b/homeassistant/components/solarman/entity.py @@ -0,0 +1,34 @@ +"""Base entity for the Solarman integration.""" + +from __future__ import annotations + +from homeassistant.const import CONF_MAC, CONF_MODEL +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import CONF_SN, DOMAIN, MODEL_NAME_MAP +from .coordinator import SolarmanDeviceUpdateCoordinator + + +class SolarmanEntity(CoordinatorEntity[SolarmanDeviceUpdateCoordinator]): + """Defines a Solarman entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: SolarmanDeviceUpdateCoordinator) -> None: + """Initialize the Solarman entity.""" + super().__init__(coordinator) + + entry = coordinator.config_entry + + sn = entry.data[CONF_SN] + model_id = entry.data[CONF_MODEL] + + self._attr_device_info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, entry.data[CONF_MAC])}, + identifiers={(DOMAIN, sn)}, + manufacturer="SOLARMAN", + model=MODEL_NAME_MAP[model_id], + model_id=model_id, + serial_number=sn, + ) diff --git a/homeassistant/components/solarman/manifest.json b/homeassistant/components/solarman/manifest.json new file mode 100644 index 00000000000..770bb5d0877 --- /dev/null +++ b/homeassistant/components/solarman/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "solarman", + "name": "Solarman", + "codeowners": ["@solarmanpv"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/solarman", + "integration_type": "device", + "iot_class": "local_polling", + "quality_scale": "bronze", + "requirements": ["solarman-opendata==0.0.3"], + "zeroconf": ["_solarman._tcp.local."] +} diff --git a/homeassistant/components/solarman/quality_scale.yaml b/homeassistant/components/solarman/quality_scale.yaml new file mode 100644 index 00000000000..2aed898e415 --- /dev/null +++ b/homeassistant/components/solarman/quality_scale.yaml @@ -0,0 +1,85 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: The integration does not provide additional actions. + appropriate-polling: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: + status: exempt + comment: The integration does not provide additional actions. + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: | + Entities of this integration does not explicitly subscribe to events. + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: + status: exempt + comment: The integration does not provide additional actions. + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: | + This integration does not have an options flow. + docs-installation-parameters: todo + entity-unavailable: done + integration-owner: todo + log-when-unavailable: done + parallel-updates: done + reauthentication-flow: + status: exempt + comment: | + No authentication required. + test-coverage: todo + # Gold + devices: done + diagnostics: todo + discovery-update-info: done + discovery: done + docs-data-update: todo + docs-examples: todo + docs-known-limitations: todo + docs-supported-devices: todo + docs-supported-functions: todo + docs-troubleshooting: todo + docs-use-cases: todo + dynamic-devices: + status: exempt + comment: The integration connects to a single device. + entity-category: todo + entity-device-class: done + entity-disabled-by-default: + status: exempt + comment: | + No noisy entities. + entity-translations: done + exception-translations: todo + icon-translations: todo + reconfiguration-flow: todo + repair-issues: + status: exempt + comment: | + No repairs/issues. + stale-devices: + status: exempt + comment: | + Device type integration. + + # Platinum + async-dependency: done + inject-websession: done + strict-typing: todo diff --git a/homeassistant/components/solarman/sensor.py b/homeassistant/components/solarman/sensor.py new file mode 100644 index 00000000000..2bca637ea2e --- /dev/null +++ b/homeassistant/components/solarman/sensor.py @@ -0,0 +1,281 @@ +"""Sensor platform for Solarman.""" + +from typing import Final + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + PERCENTAGE, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfPower, + UnitOfVolume, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .coordinator import SolarmanConfigEntry, SolarmanDeviceUpdateCoordinator +from .entity import SolarmanEntity + +PARALLEL_UPDATES = 0 + + +SENSORS: Final = ( + SensorEntityDescription( + key="voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electric_current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="positive_active_energy", + translation_key="positive_active_energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="reverse_active_energy", + translation_key="reverse_active_energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="total_act_energy_LT", + translation_key="total_act_energy_lt", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="total_act_energy_NT", + translation_key="total_act_energy_nt", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="total_act_ret_energy_LT", + translation_key="total_act_ret_energy_lt", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="total_act_ret_energy_NT", + translation_key="total_act_ret_energy_nt", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="a_current", + translation_key="a_current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="b_current", + translation_key="b_current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="c_current", + translation_key="c_current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="a_voltage", + translation_key="a_voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="b_voltage", + translation_key="b_voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="c_voltage", + translation_key="c_voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="total_act_power", + translation_key="total_act_power", + native_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="total_act_ret_power", + translation_key="total_act_ret_power", + native_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="a_act_power", + translation_key="a_act_power", + native_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="b_act_power", + translation_key="b_act_power", + native_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="c_act_power", + translation_key="c_act_power", + native_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="a_act_ret_power", + translation_key="a_act_ret_power", + native_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="b_act_ret_power", + translation_key="b_act_ret_power", + native_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="c_act_ret_power", + translation_key="c_act_ret_power", + native_unit_of_measurement=UnitOfPower.KILO_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="total_gas", + translation_key="total_gas", + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="active power", + translation_key="active_power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="apparent power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.APPARENT_POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="reactive power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.REACTIVE_POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power factor", + translation_key="power_factor", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="frequency", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="total_act_energy", + translation_key="total_act_energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="total_act_ret_energy", + translation_key="total_act_ret_energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: SolarmanConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up sensors from SolarmanConfigEntry.""" + async_add_entities( + SolarmanSensorEntity(entry.runtime_data, description) + for description in SENSORS + if description.key in entry.runtime_data.data + ) + + +class SolarmanSensorEntity(SolarmanEntity, SensorEntity): + """Representation of a Solarman sensor.""" + + def __init__( + self, + coordinator: SolarmanDeviceUpdateCoordinator, + description: SensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + assert coordinator.config_entry + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}" + + @property + def native_value(self) -> StateType: + """Return value of sensor.""" + return self.coordinator.data[self.entity_description.key] diff --git a/homeassistant/components/solarman/strings.json b/homeassistant/components/solarman/strings.json new file mode 100644 index 00000000000..2d57c3998e1 --- /dev/null +++ b/homeassistant/components/solarman/strings.json @@ -0,0 +1,114 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "another_device": "The configured device is not the same found on this IP address", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout": "Connection timed out while trying to connect to the device" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout": "[%key:common::config_flow::error::timeout_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "discovery_confirm": { + "description": "Do you want to set up {model} ({sn}) at {host}?" + }, + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The IP address or hostname of the Solarman device." + }, + "description": "Enter the IP address of Solarman device to integrate with Home Assistant.", + "title": "Configure Solarman Device" + } + } + }, + "entity": { + "sensor": { + "a_act_power": { + "name": "Active power phase-A" + }, + "a_act_ret_power": { + "name": "Active returned power phase-A" + }, + "a_current": { + "name": "AC Phase-A current" + }, + "a_voltage": { + "name": "AC Phase-A voltage" + }, + "active_power": { + "name": "Active power" + }, + "b_act_power": { + "name": "Active power phase-B" + }, + "b_act_ret_power": { + "name": "Active returned power phase-B" + }, + "b_current": { + "name": "AC Phase-B current" + }, + "b_voltage": { + "name": "AC Phase-B voltage" + }, + "c_act_power": { + "name": "Active power phase-C" + }, + "c_act_ret_power": { + "name": "Active returned power phase-C" + }, + "c_current": { + "name": "AC Phase-C current" + }, + "c_voltage": { + "name": "AC Phase-C voltage" + }, + "positive_active_energy": { + "name": "Positive active energy" + }, + "power_factor": { + "name": "Power factor" + }, + "reverse_active_energy": { + "name": "Reverse active energy" + }, + "total_act_energy": { + "name": "Total actual energy" + }, + "total_act_energy_lt": { + "name": "Total actual energy low tariff" + }, + "total_act_energy_nt": { + "name": "Total actual energy normal tariff" + }, + "total_act_power": { + "name": "Total actual power" + }, + "total_act_ret_energy": { + "name": "Total actual returned energy" + }, + "total_act_ret_energy_lt": { + "name": "Total actual returned energy low tariff" + }, + "total_act_ret_energy_nt": { + "name": "Total actual returned energy normal tariff" + }, + "total_act_ret_power": { + "name": "Total actual returned power" + }, + "total_gas": { + "name": "Total gas consumption" + } + } + }, + "exceptions": { + "update_failed": { + "message": "Unable to fetch data." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4634da3a687..793cfa1cc65 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -665,6 +665,7 @@ FLOWS = { "snooz", "solaredge", "solarlog", + "solarman", "solax", "soma", "somfy_mylink", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index f003126a4e3..1c577775c44 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6475,6 +6475,12 @@ "config_flow": true, "iot_class": "local_polling" }, + "solarman": { + "name": "Solarman", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling" + }, "solax": { "name": "SolaX Power", "integration_type": "device", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 8cd43f195af..50bb4f31414 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -932,6 +932,11 @@ ZEROCONF = { "domain": "cambridge_audio", }, ], + "_solarman._tcp.local.": [ + { + "domain": "solarman", + }, + ], "_sonos._tcp.local.": [ { "domain": "sonos", diff --git a/requirements_all.txt b/requirements_all.txt index ae0bccc7dfe..07e207960b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2983,6 +2983,9 @@ solaredge-web==0.0.1 # homeassistant.components.solarlog solarlog_cli==0.7.0 +# homeassistant.components.solarman +solarman-opendata==0.0.3 + # homeassistant.components.solax solax==3.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d39d8eab7c..f2e84355bdf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2522,6 +2522,9 @@ solaredge-web==0.0.1 # homeassistant.components.solarlog solarlog_cli==0.7.0 +# homeassistant.components.solarman +solarman-opendata==0.0.3 + # homeassistant.components.solax solax==3.2.3 diff --git a/tests/components/solarman/__init__.py b/tests/components/solarman/__init__.py new file mode 100644 index 00000000000..84a8edb9a49 --- /dev/null +++ b/tests/components/solarman/__init__.py @@ -0,0 +1,14 @@ +"""Tests for the Solarman integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Set up the integration for testing.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/solarman/conftest.py b/tests/components/solarman/conftest.py new file mode 100644 index 00000000000..7d1cd091380 --- /dev/null +++ b/tests/components/solarman/conftest.py @@ -0,0 +1,71 @@ +"""Common fixtures for the Solarman tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.solarman.const import CONF_SN, DOMAIN, MODEL_NAME_MAP +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODEL + +from tests.common import MockConfigEntry, load_json_object_fixture + +TEST_HOST = "192.168.1.100" +TEST_PORT = 8080 +TEST_DEVICE_SN = "SN1234567890" +TEST_MODEL = "SP-2W-EU" +TEST_MAC = "AA:BB:CC:DD:EE:FF" + + +@pytest.fixture +def device_fixture(request: pytest.FixtureRequest) -> str | None: + """Return the device fixtures for a specific device.""" + return getattr(request, "param", None) + + +@pytest.fixture +def mock_config_entry(device_fixture: str) -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title=f"{MODEL_NAME_MAP[device_fixture]} ({TEST_HOST})", + data={ + CONF_HOST: TEST_HOST, + CONF_SN: TEST_DEVICE_SN, + CONF_MODEL: device_fixture, + CONF_MAC: TEST_MAC, + }, + unique_id=TEST_DEVICE_SN, + ) + + +@pytest.fixture +def mock_solarman(device_fixture: str) -> Generator[AsyncMock]: + """Mock a solarman client.""" + with ( + patch( + "homeassistant.components.solarman.coordinator.Solarman", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.solarman.config_flow.Solarman", + new=mock_client, + ), + ): + client = mock_client.return_value + client.get_config.return_value = load_json_object_fixture( + f"{device_fixture}/config.json", DOMAIN + ) + client.fetch_data.return_value = load_json_object_fixture( + f"{device_fixture}/data.json", DOMAIN + ) + yield client + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.solarman.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/solarman/fixtures/P1-2W/config.json b/tests/components/solarman/fixtures/P1-2W/config.json new file mode 100644 index 00000000000..52b7d731b8b --- /dev/null +++ b/tests/components/solarman/fixtures/P1-2W/config.json @@ -0,0 +1,13 @@ +{ + "device": { + "hostname": "admin", + "timezone": 480, + "type": "P1-2W", + "sn": "SN2345678901", + "mac": "MAC234567891", + "fw": "LSW3_01_E030_SS_00_00.00.00.03", + "time": "2024-11-26 19:14:23", + "time_stamp": 1732648463, + "run_time": 839 + } +} diff --git a/tests/components/solarman/fixtures/P1-2W/data.json b/tests/components/solarman/fixtures/P1-2W/data.json new file mode 100644 index 00000000000..2737b9bad7c --- /dev/null +++ b/tests/components/solarman/fixtures/P1-2W/data.json @@ -0,0 +1,24 @@ +{ + "SN": "E0036003765928016", + "device_version": "50", + "Device_Type": 0, + "total_act_energy_LT": 53754.58, + "total_act_energy_NT": 9818.93, + "total_act_ret_energy_LT": 53754.58, + "total_act_ret_energy_NT": 9818.93, + "a_current": 12, + "b_current": 0, + "c_current": 0, + "a_voltage": 237, + "b_voltage": 237, + "c_voltage": 236, + "total_act_power": 0, + "total_act_ret_power": 2.94, + "a_act_power": 0, + "b_act_power": 0, + "c_act_power": 0, + "a_act_ret_power": 2.95, + "b_act_ret_power": 0, + "c_act_ret_power": 0, + "total_gas": 0 +} diff --git a/tests/components/solarman/fixtures/SP-2W-EU/config.json b/tests/components/solarman/fixtures/SP-2W-EU/config.json new file mode 100644 index 00000000000..b12e9f48008 --- /dev/null +++ b/tests/components/solarman/fixtures/SP-2W-EU/config.json @@ -0,0 +1,11 @@ +{ + "hostname": "admin", + "timezone": 480, + "type": "SP-2W-EU", + "sn": "SN1234567890", + "mac": "MAC123456789", + "fw": "LSW3_01_E030_SS_00_00.00.00.03", + "time": "2024-11-26 19:14:23", + "time_stamp": 1732648463, + "run_time": 839 +} diff --git a/tests/components/solarman/fixtures/SP-2W-EU/data.json b/tests/components/solarman/fixtures/SP-2W-EU/data.json new file mode 100644 index 00000000000..459b076ec46 --- /dev/null +++ b/tests/components/solarman/fixtures/SP-2W-EU/data.json @@ -0,0 +1,7 @@ +{ + "voltage": 230, + "electric_current": 0, + "positive_active_energy": 1.730654, + "reverse_active_energy": 0.0, + "power": 1.217517 +} diff --git a/tests/components/solarman/fixtures/gl meter/config.json b/tests/components/solarman/fixtures/gl meter/config.json new file mode 100644 index 00000000000..d497acb8ca9 --- /dev/null +++ b/tests/components/solarman/fixtures/gl meter/config.json @@ -0,0 +1,11 @@ +{ + "hostname": "admin", + "timezone": 480, + "type": "gl meter", + "sn": "SN3456789012", + "mac": "MAC345678912", + "fw": "LSW3_01_E030_SS_00_00.00.00.03", + "time": "2024-11-26 19:14:23", + "time_stamp": 1732648463, + "run_time": 839 +} diff --git a/tests/components/solarman/fixtures/gl meter/data.json b/tests/components/solarman/fixtures/gl meter/data.json new file mode 100644 index 00000000000..50be909c5ad --- /dev/null +++ b/tests/components/solarman/fixtures/gl meter/data.json @@ -0,0 +1,12 @@ +{ + "SN": "3310500113", + "voltage": 229.87, + "current": 0.66, + "active power": 133.5, + "apparent power": 209.6, + "reactive power": -29.8, + "power factor": 0.63, + "frequency": 50.03, + "total_act_energy": 3.53, + "total_act_ret_energy": 0.18 +} diff --git a/tests/components/solarman/snapshots/test_sensor.ambr b/tests/components/solarman/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..4348ea207b6 --- /dev/null +++ b/tests/components/solarman/snapshots/test_sensor.ambr @@ -0,0 +1,1906 @@ +# serializer version: 1 +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_ac_phase_a_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_ac_phase_a_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'AC Phase-A current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC Phase-A current', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'a_current', + 'unique_id': 'SN1234567890_a_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_ac_phase_a_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) AC Phase-A current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_ac_phase_a_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_ac_phase_a_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_ac_phase_a_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'AC Phase-A voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC Phase-A voltage', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'a_voltage', + 'unique_id': 'SN1234567890_a_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_ac_phase_a_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) AC Phase-A voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_ac_phase_a_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '237', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_ac_phase_b_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_ac_phase_b_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'AC Phase-B current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC Phase-B current', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'b_current', + 'unique_id': 'SN1234567890_b_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_ac_phase_b_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) AC Phase-B current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_ac_phase_b_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_ac_phase_b_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_ac_phase_b_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'AC Phase-B voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC Phase-B voltage', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'b_voltage', + 'unique_id': 'SN1234567890_b_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_ac_phase_b_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) AC Phase-B voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_ac_phase_b_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '237', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_ac_phase_c_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_ac_phase_c_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'AC Phase-C current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC Phase-C current', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'c_current', + 'unique_id': 'SN1234567890_c_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_ac_phase_c_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) AC Phase-C current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_ac_phase_c_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_ac_phase_c_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_ac_phase_c_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'AC Phase-C voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'AC Phase-C voltage', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'c_voltage', + 'unique_id': 'SN1234567890_c_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_ac_phase_c_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) AC Phase-C voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_ac_phase_c_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '236', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_active_power_phase_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_active_power_phase_a', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Active power phase-A', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Active power phase-A', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'a_act_power', + 'unique_id': 'SN1234567890_a_act_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_active_power_phase_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Active power phase-A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_active_power_phase_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_active_power_phase_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_active_power_phase_b', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Active power phase-B', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Active power phase-B', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'b_act_power', + 'unique_id': 'SN1234567890_b_act_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_active_power_phase_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Active power phase-B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_active_power_phase_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_active_power_phase_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_active_power_phase_c', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Active power phase-C', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Active power phase-C', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'c_act_power', + 'unique_id': 'SN1234567890_c_act_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_active_power_phase_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Active power phase-C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_active_power_phase_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_active_returned_power_phase_a-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_active_returned_power_phase_a', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Active returned power phase-A', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Active returned power phase-A', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'a_act_ret_power', + 'unique_id': 'SN1234567890_a_act_ret_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_active_returned_power_phase_a-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Active returned power phase-A', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_active_returned_power_phase_a', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.95', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_active_returned_power_phase_b-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_active_returned_power_phase_b', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Active returned power phase-B', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Active returned power phase-B', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'b_act_ret_power', + 'unique_id': 'SN1234567890_b_act_ret_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_active_returned_power_phase_b-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Active returned power phase-B', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_active_returned_power_phase_b', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_active_returned_power_phase_c-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_active_returned_power_phase_c', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Active returned power phase-C', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Active returned power phase-C', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'c_act_ret_power', + 'unique_id': 'SN1234567890_c_act_ret_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_active_returned_power_phase_c-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Active returned power phase-C', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_active_returned_power_phase_c', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_actual_energy_low_tariff-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_actual_energy_low_tariff', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total actual energy low tariff', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total actual energy low tariff', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_act_energy_lt', + 'unique_id': 'SN1234567890_total_act_energy_LT', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_actual_energy_low_tariff-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Total actual energy low tariff', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_actual_energy_low_tariff', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '53754.58', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_actual_energy_normal_tariff-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_actual_energy_normal_tariff', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total actual energy normal tariff', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total actual energy normal tariff', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_act_energy_nt', + 'unique_id': 'SN1234567890_total_act_energy_NT', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_actual_energy_normal_tariff-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Total actual energy normal tariff', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_actual_energy_normal_tariff', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9818.93', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_actual_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_actual_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total actual power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total actual power', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_act_power', + 'unique_id': 'SN1234567890_total_act_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_actual_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Total actual power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_actual_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_actual_returned_energy_low_tariff-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_actual_returned_energy_low_tariff', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total actual returned energy low tariff', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total actual returned energy low tariff', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_act_ret_energy_lt', + 'unique_id': 'SN1234567890_total_act_ret_energy_LT', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_actual_returned_energy_low_tariff-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Total actual returned energy low tariff', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_actual_returned_energy_low_tariff', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '53754.58', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_actual_returned_energy_normal_tariff-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_actual_returned_energy_normal_tariff', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total actual returned energy normal tariff', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total actual returned energy normal tariff', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_act_ret_energy_nt', + 'unique_id': 'SN1234567890_total_act_ret_energy_NT', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_actual_returned_energy_normal_tariff-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Total actual returned energy normal tariff', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_actual_returned_energy_normal_tariff', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9818.93', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_actual_returned_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_actual_returned_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total actual returned power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total actual returned power', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_act_ret_power', + 'unique_id': 'SN1234567890_total_act_ret_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_actual_returned_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Total actual returned power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_actual_returned_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.94', + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_gas_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_gas_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total gas consumption', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total gas consumption', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_gas', + 'unique_id': 'SN1234567890_total_gas', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[P1-2W][sensor.p1_meter_reader_192_168_1_100_total_gas_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gas', + 'friendly_name': 'P1 Meter Reader (192.168.1.100) Total gas consumption', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.p1_meter_reader_192_168_1_100_total_gas_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[SP-2W-EU][sensor.smart_plug_192_168_1_100_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_plug_192_168_1_100_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'SN1234567890_electric_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[SP-2W-EU][sensor.smart_plug_192_168_1_100_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Smart Plug (192.168.1.100) Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_plug_192_168_1_100_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_sensor[SP-2W-EU][sensor.smart_plug_192_168_1_100_positive_active_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_plug_192_168_1_100_positive_active_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Positive active energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Positive active energy', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'positive_active_energy', + 'unique_id': 'SN1234567890_positive_active_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[SP-2W-EU][sensor.smart_plug_192_168_1_100_positive_active_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Smart Plug (192.168.1.100) Positive active energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_plug_192_168_1_100_positive_active_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.730654', + }) +# --- +# name: test_sensor[SP-2W-EU][sensor.smart_plug_192_168_1_100_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_plug_192_168_1_100_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'SN1234567890_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[SP-2W-EU][sensor.smart_plug_192_168_1_100_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Smart Plug (192.168.1.100) Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_plug_192_168_1_100_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.217517', + }) +# --- +# name: test_sensor[SP-2W-EU][sensor.smart_plug_192_168_1_100_reverse_active_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_plug_192_168_1_100_reverse_active_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Reverse active energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Reverse active energy', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'reverse_active_energy', + 'unique_id': 'SN1234567890_reverse_active_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[SP-2W-EU][sensor.smart_plug_192_168_1_100_reverse_active_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Smart Plug (192.168.1.100) Reverse active energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_plug_192_168_1_100_reverse_active_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensor[SP-2W-EU][sensor.smart_plug_192_168_1_100_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_plug_192_168_1_100_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'SN1234567890_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[SP-2W-EU][sensor.smart_plug_192_168_1_100_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Smart Plug (192.168.1.100) Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_plug_192_168_1_100_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '230', + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_active_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_meter_192_168_1_100_active_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Active power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Active power', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'active_power', + 'unique_id': 'SN1234567890_active power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_active_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Smart Meter (192.168.1.100) Active power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_meter_192_168_1_100_active_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '133.5', + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_apparent_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_meter_192_168_1_100_apparent_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Apparent power', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Apparent power', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'SN1234567890_apparent power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_apparent_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'apparent_power', + 'friendly_name': 'Smart Meter (192.168.1.100) Apparent power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_meter_192_168_1_100_apparent_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '209.6', + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_current-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_meter_192_168_1_100_current', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Current', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'SN1234567890_current', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_current-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'current', + 'friendly_name': 'Smart Meter (192.168.1.100) Current', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_meter_192_168_1_100_current', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.66', + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_frequency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_meter_192_168_1_100_frequency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Frequency', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Frequency', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'SN1234567890_frequency', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_frequency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'frequency', + 'friendly_name': 'Smart Meter (192.168.1.100) Frequency', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_meter_192_168_1_100_frequency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50.03', + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_power_factor-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_meter_192_168_1_100_power_factor', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Power factor', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power factor', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power_factor', + 'unique_id': 'SN1234567890_power factor', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_power_factor-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Smart Meter (192.168.1.100) Power factor', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.smart_meter_192_168_1_100_power_factor', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.63', + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_reactive_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_meter_192_168_1_100_reactive_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Reactive power', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Reactive power', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'SN1234567890_reactive power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_reactive_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'reactive_power', + 'friendly_name': 'Smart Meter (192.168.1.100) Reactive power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_meter_192_168_1_100_reactive_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-29.8', + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_total_actual_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_meter_192_168_1_100_total_actual_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total actual energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total actual energy', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_act_energy', + 'unique_id': 'SN1234567890_total_act_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_total_actual_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Smart Meter (192.168.1.100) Total actual energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_meter_192_168_1_100_total_actual_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.53', + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_total_actual_returned_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_meter_192_168_1_100_total_actual_returned_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Total actual returned energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total actual returned energy', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_act_ret_energy', + 'unique_id': 'SN1234567890_total_act_ret_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_total_actual_returned_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Smart Meter (192.168.1.100) Total actual returned energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_meter_192_168_1_100_total_actual_returned_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.18', + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_voltage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.smart_meter_192_168_1_100_voltage', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Voltage', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Voltage', + 'platform': 'solarman', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'SN1234567890_voltage', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[gl meter][sensor.smart_meter_192_168_1_100_voltage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', + 'friendly_name': 'Smart Meter (192.168.1.100) Voltage', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.smart_meter_192_168_1_100_voltage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '229.87', + }) +# --- diff --git a/tests/components/solarman/test_config_flow.py b/tests/components/solarman/test_config_flow.py new file mode 100644 index 00000000000..b3ca1b0b7fb --- /dev/null +++ b/tests/components/solarman/test_config_flow.py @@ -0,0 +1,252 @@ +"""Test the Solarman config flow.""" + +from ipaddress import ip_address +from unittest.mock import AsyncMock + +import pytest + +from homeassistant.components.solarman.const import CONF_SN, DOMAIN, MODEL_NAME_MAP +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_MODEL +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo + +from tests.common import MockConfigEntry + +# Configure test constants +TEST_HOST = "192.168.1.100" +TEST_DEVICE_SN = "SN1234567890" +TEST_MODEL = "SP-2W-EU" + + +@pytest.mark.parametrize("device_fixture", ["P1-2W"], indirect=True) +async def test_flow_success(hass: HomeAssistant, mock_solarman: AsyncMock) -> None: + """Test successful configuration flow.""" + + # Initiate config flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + # Verify initial form + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + # Submit valid data + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: TEST_HOST} + ) + + # Verify entry creation + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == f"P1 Meter Reader ({TEST_HOST})" + assert result["context"]["unique_id"] == "SN2345678901" + + # Verify configuration data. + data = result["data"] + assert data[CONF_HOST] == TEST_HOST + assert data[CONF_SN] == "SN2345678901" + assert data[CONF_MODEL] == "P1-2W" + + +@pytest.mark.parametrize("device_fixture", ["SP-2W-EU"], indirect=True) +@pytest.mark.parametrize( + ("exception", "expected_error"), + [ + (TimeoutError, "timeout"), + (ConnectionError, "cannot_connect"), + (Exception("Some unknown error"), "unknown"), + ], +) +async def test_flow_error( + hass: HomeAssistant, + mock_solarman: AsyncMock, + exception: Exception, + expected_error: str, +) -> None: + """Test connection error handling.""" + # Initiate and submit config flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + mock_solarman.get_config.side_effect = exception + + # Submit config flow + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST}, + ) + + # Verify error form display. + assert result["type"] is FlowResultType.FORM + assert result["errors"]["base"] == expected_error + + mock_solarman.get_config.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST}, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + + +@pytest.mark.parametrize("device_fixture", ["SP-2W-EU"], indirect=True) +async def test_flow_duplicate_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_solarman: AsyncMock +) -> None: + """Test duplicate entry handling.""" + # Create existing config entry. + mock_config_entry.add_to_hass(hass) + + # Initiate and submit config flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: TEST_HOST} + ) + + # Verify flow abort + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize("device_fixture", ["SP-2W-EU"], indirect=True) +async def test_zeroconf(hass: HomeAssistant, mock_solarman: AsyncMock) -> None: + """Test zeroconf discovery flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address(TEST_HOST), + ip_addresses=[ip_address(TEST_HOST)], + name="mock_name", + port=8080, + hostname="mock_hostname", + type="_solarman._tcp.local.", + properties={ + "product_type": "SP-2W-EU", + "serial": TEST_DEVICE_SN, + }, + ), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == f"{MODEL_NAME_MAP[TEST_MODEL]} ({TEST_HOST})" + + data = result["data"] + assert data[CONF_HOST] == TEST_HOST + assert data[CONF_SN] == TEST_DEVICE_SN + assert data[CONF_MODEL] == TEST_MODEL + assert result["context"]["unique_id"] == TEST_DEVICE_SN + + +@pytest.mark.parametrize("device_fixture", ["SP-2W-EU"], indirect=True) +async def test_zeroconf_already_configured( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_solarman: AsyncMock, +) -> None: + """Test zeroconf discovery when already configured.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address(TEST_HOST), + ip_addresses=[ip_address(TEST_HOST)], + name="mock_name", + port=8080, + hostname="mock_hostname", + type="_solarman._tcp.local.", + properties={ + "product_type": "SP-2W-EU", + "serial": TEST_DEVICE_SN, + }, + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize("device_fixture", ["SP-2W-EU"], indirect=True) +async def test_zeroconf_ip_change( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_solarman: AsyncMock +) -> None: + """Test discovery setup updates new config data.""" + mock_config_entry.add_to_hass(hass) + + # preflight check, see if the ip address is already in use + assert mock_config_entry.data[CONF_HOST] == TEST_HOST + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.101"), + ip_addresses=[ip_address("192.168.1.101")], + name="mock_name", + port=8080, + hostname="mock_hostname", + type="_solarman._tcp.local.", + properties={ + "product_type": "SP-2W-EU", + "serial": TEST_DEVICE_SN, + }, + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + assert mock_config_entry.data[CONF_HOST] == "192.168.1.101" + + +@pytest.mark.parametrize( + ("exception", "expected_error"), + [ + (TimeoutError, "timeout"), + (ConnectionError, "cannot_connect"), + (Exception("Some unknown error"), "unknown"), + ], +) +@pytest.mark.parametrize("device_fixture", ["SP-2W-EU"], indirect=True) +async def test_zeroconf_error( + hass: HomeAssistant, + mock_solarman: AsyncMock, + exception: Exception, + expected_error: str, +) -> None: + """Test discovery setup.""" + mock_solarman.get_config.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + ip_address=ip_address(TEST_HOST), + ip_addresses=[ip_address(TEST_HOST)], + name="mock_name", + port=8080, + hostname="mock_hostname", + type="_solarman._tcp.local.", + properties={ + "product_type": "SP-2W-EU", + "serial": TEST_DEVICE_SN, + }, + ), + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == expected_error diff --git a/tests/components/solarman/test_init.py b/tests/components/solarman/test_init.py new file mode 100644 index 00000000000..c8ad92ab1d8 --- /dev/null +++ b/tests/components/solarman/test_init.py @@ -0,0 +1,52 @@ +"""Test init of Solarman integration.""" + +from unittest.mock import AsyncMock + +import pytest + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("device_fixture", ["SP-2W-EU"], indirect=True) +async def test_load_unload( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_solarman: AsyncMock, +) -> None: + """Test setting up and removing a config entry.""" + + # Add the mock config entry to Home Assistant + mock_config_entry.add_to_hass(hass) + + # Set up the integration using the config entry + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + # Wait for all background tasks to complete + await hass.async_block_till_done() + + # Verify the config entry is successfully loaded + assert mock_config_entry.state is ConfigEntryState.LOADED + + # Unload the integration + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Verify the config entry is properly unloaded + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize("device_fixture", ["SP-2W-EU"], indirect=True) +async def test_load_failure( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_solarman: AsyncMock +) -> None: + """Test setup failure.""" + mock_solarman.fetch_data.side_effect = TimeoutError + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Verify the config entry enters retry state due to failure + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/solarman/test_sensor.py b/tests/components/solarman/test_sensor.py new file mode 100644 index 00000000000..ab8f3a103f4 --- /dev/null +++ b/tests/components/solarman/test_sensor.py @@ -0,0 +1,58 @@ +"""Tests for the Solarman sensor device.""" + +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.solarman.const import UPDATE_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + "device_fixture", ["P1-2W", "SP-2W-EU", "gl meter"], indirect=True +) +async def test_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_solarman: AsyncMock, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test sensor platform.""" + with patch("homeassistant.components.solarman.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize("device_fixture", ["SP-2W-EU"], indirect=True) +async def test_sensor_availability( + hass: HomeAssistant, + mock_solarman: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sensor availability.""" + with patch("homeassistant.components.solarman.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get("sensor.smart_plug_192_168_1_100_voltage")) + assert state.state == "230" + + mock_solarman.fetch_data.side_effect = ConnectionError + freezer.tick(UPDATE_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get("sensor.smart_plug_192_168_1_100_voltage")) + assert state.state == STATE_UNAVAILABLE