mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add Altruist integration to Core (#146158)
* add altruist integration and tests * requested fixes + remove some deprecated sensors * add tests for unknown sensor and device attribute in config_flow * use CONF_ in data_schema * suggested fixes * remove test_setup_entry_success * create ZeroconfServiceInfo in tests * use CONF_IP_ADDRESS in tests * add unique id assert * add integration to strict-typing, set unavailable if no sensor key in data, change device name * use add_suggested_values_to_schema, mmHg for pressure * update snapshots and config entry name in tests * remove changes in devcontainer config * fixture for create client error, typing in tests, remove "Altruist" from device name * change native_value_fn return type * change sensor.py docstring * remove device id from entry data, fix docstrings * remove checks for client and device attributes * use less variables in tests * change creating AltruistSensor, remove device from arguments * Update homeassistant/components/altruist/sensor.py * Update homeassistant/components/altruist/quality_scale.yaml * Update homeassistant/components/altruist/quality_scale.yaml * Update quality_scale.yaml * hassfest run * suggested fixes * set suggested_unit_of_measurement for pressure * use mock_config_entry, update snapshots * abort if cant create client on zeroconf step * move sensor names in translatin placeholders --------- Co-authored-by: Josef Zweck <josef@zweck.dev>
This commit is contained in:
parent
3798e99ac8
commit
f8267b13d7
@ -67,6 +67,7 @@ homeassistant.components.alert.*
|
|||||||
homeassistant.components.alexa.*
|
homeassistant.components.alexa.*
|
||||||
homeassistant.components.alexa_devices.*
|
homeassistant.components.alexa_devices.*
|
||||||
homeassistant.components.alpha_vantage.*
|
homeassistant.components.alpha_vantage.*
|
||||||
|
homeassistant.components.altruist.*
|
||||||
homeassistant.components.amazon_polly.*
|
homeassistant.components.amazon_polly.*
|
||||||
homeassistant.components.amberelectric.*
|
homeassistant.components.amberelectric.*
|
||||||
homeassistant.components.ambient_network.*
|
homeassistant.components.ambient_network.*
|
||||||
|
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@ -93,6 +93,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||||
/homeassistant/components/alexa_devices/ @chemelli74
|
/homeassistant/components/alexa_devices/ @chemelli74
|
||||||
/tests/components/alexa_devices/ @chemelli74
|
/tests/components/alexa_devices/ @chemelli74
|
||||||
|
/homeassistant/components/altruist/ @airalab @LoSk-p
|
||||||
|
/tests/components/altruist/ @airalab @LoSk-p
|
||||||
/homeassistant/components/amazon_polly/ @jschlyter
|
/homeassistant/components/amazon_polly/ @jschlyter
|
||||||
/homeassistant/components/amberelectric/ @madpilot
|
/homeassistant/components/amberelectric/ @madpilot
|
||||||
/tests/components/amberelectric/ @madpilot
|
/tests/components/amberelectric/ @madpilot
|
||||||
|
27
homeassistant/components/altruist/__init__.py
Normal file
27
homeassistant/components/altruist/__init__.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"""The Altruist integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import AltruistConfigEntry, AltruistDataUpdateCoordinator
|
||||||
|
|
||||||
|
PLATFORMS = [Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: AltruistConfigEntry) -> bool:
|
||||||
|
"""Set up Altruist from a config entry."""
|
||||||
|
|
||||||
|
coordinator = AltruistDataUpdateCoordinator(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: AltruistConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
107
homeassistant/components/altruist/config_flow.py
Normal file
107
homeassistant/components/altruist/config_flow.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
"""Config flow for the Altruist integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from altruistclient import AltruistClient, AltruistDeviceModel, AltruistError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
|
||||||
|
from .const import CONF_HOST, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AltruistConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Altruist."""
|
||||||
|
|
||||||
|
device: AltruistDeviceModel
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
ip_address = ""
|
||||||
|
if user_input is not None:
|
||||||
|
ip_address = user_input[CONF_HOST]
|
||||||
|
try:
|
||||||
|
client = await AltruistClient.from_ip_address(
|
||||||
|
async_get_clientsession(self.hass), ip_address
|
||||||
|
)
|
||||||
|
except AltruistError:
|
||||||
|
errors["base"] = "no_device_found"
|
||||||
|
else:
|
||||||
|
self.device = client.device
|
||||||
|
await self.async_set_unique_id(
|
||||||
|
client.device_id, raise_on_progress=False
|
||||||
|
)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self.device.id,
|
||||||
|
data={
|
||||||
|
CONF_HOST: ip_address,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
data_schema = self.add_suggested_values_to_schema(
|
||||||
|
vol.Schema({vol.Required(CONF_HOST): str}),
|
||||||
|
{CONF_HOST: ip_address},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=data_schema,
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"ip_address": ip_address,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_zeroconf(
|
||||||
|
self, discovery_info: ZeroconfServiceInfo
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle zeroconf discovery."""
|
||||||
|
_LOGGER.debug("Zeroconf discovery: %s", discovery_info)
|
||||||
|
try:
|
||||||
|
client = await AltruistClient.from_ip_address(
|
||||||
|
async_get_clientsession(self.hass), str(discovery_info.ip_address)
|
||||||
|
)
|
||||||
|
except AltruistError:
|
||||||
|
return self.async_abort(reason="no_device_found")
|
||||||
|
|
||||||
|
self.device = client.device
|
||||||
|
_LOGGER.debug("Zeroconf device: %s", client.device)
|
||||||
|
await self.async_set_unique_id(client.device_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
self.context.update(
|
||||||
|
{
|
||||||
|
"title_placeholders": {
|
||||||
|
"name": self.device.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return await self.async_step_discovery_confirm()
|
||||||
|
|
||||||
|
async def async_step_discovery_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Confirm discovery."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self.device.id,
|
||||||
|
data={
|
||||||
|
CONF_HOST: self.device.ip_address,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self._set_confirm_only()
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="discovery_confirm",
|
||||||
|
description_placeholders={
|
||||||
|
"model": self.device.id,
|
||||||
|
},
|
||||||
|
)
|
5
homeassistant/components/altruist/const.py
Normal file
5
homeassistant/components/altruist/const.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""Constants for the Altruist integration."""
|
||||||
|
|
||||||
|
DOMAIN = "altruist"
|
||||||
|
|
||||||
|
CONF_HOST = "host"
|
64
homeassistant/components/altruist/coordinator.py
Normal file
64
homeassistant/components/altruist/coordinator.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
"""Coordinator module for Altruist integration in Home Assistant.
|
||||||
|
|
||||||
|
This module defines the AltruistDataUpdateCoordinator class, which manages
|
||||||
|
data updates for Altruist sensors using the AltruistClient.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from altruistclient import AltruistClient, AltruistError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import CONF_HOST
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
UPDATE_INTERVAL = timedelta(seconds=15)
|
||||||
|
|
||||||
|
type AltruistConfigEntry = ConfigEntry[AltruistDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class AltruistDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||||
|
"""Coordinates data updates for Altruist sensors."""
|
||||||
|
|
||||||
|
client: AltruistClient
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AltruistConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the data update coordinator for Altruist sensors."""
|
||||||
|
device_id = config_entry.unique_id
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
logger=_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name=f"Altruist {device_id}",
|
||||||
|
update_interval=UPDATE_INTERVAL,
|
||||||
|
)
|
||||||
|
self._ip_address = config_entry.data[CONF_HOST]
|
||||||
|
|
||||||
|
async def _async_setup(self) -> None:
|
||||||
|
try:
|
||||||
|
self.client = await AltruistClient.from_ip_address(
|
||||||
|
async_get_clientsession(self.hass), self._ip_address
|
||||||
|
)
|
||||||
|
await self.client.fetch_data()
|
||||||
|
except AltruistError as e:
|
||||||
|
raise ConfigEntryNotReady("Error in Altruist setup") from e
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, str]:
|
||||||
|
try:
|
||||||
|
fetched_data = await self.client.fetch_data()
|
||||||
|
except AltruistError as ex:
|
||||||
|
raise UpdateFailed(
|
||||||
|
f"The Altruist {self.client.device_id} is unavailable: {ex}"
|
||||||
|
) from ex
|
||||||
|
return {item["value_type"]: item["value"] for item in fetched_data}
|
15
homeassistant/components/altruist/icons.json
Normal file
15
homeassistant/components/altruist/icons.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"pm_10": {
|
||||||
|
"default": "mdi:thought-bubble"
|
||||||
|
},
|
||||||
|
"pm_25": {
|
||||||
|
"default": "mdi:thought-bubble-outline"
|
||||||
|
},
|
||||||
|
"radiation": {
|
||||||
|
"default": "mdi:radioactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
homeassistant/components/altruist/manifest.json
Normal file
12
homeassistant/components/altruist/manifest.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"domain": "altruist",
|
||||||
|
"name": "Altruist",
|
||||||
|
"codeowners": ["@airalab", "@LoSk-p"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/altruist",
|
||||||
|
"integration_type": "device",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["altruistclient==0.1.1"],
|
||||||
|
"zeroconf": ["_altruist._tcp.local."]
|
||||||
|
}
|
83
homeassistant/components/altruist/quality_scale.yaml
Normal file
83
homeassistant/components/altruist/quality_scale.yaml
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This 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: |
|
||||||
|
This 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: |
|
||||||
|
This integration does not provide additional actions.
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration does not provide options flow.
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable: done
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable: done
|
||||||
|
parallel-updates: todo
|
||||||
|
reauthentication-flow: todo
|
||||||
|
test-coverage: done
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info: todo
|
||||||
|
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: |
|
||||||
|
Device type integration
|
||||||
|
entity-category: todo
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default: todo
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: todo
|
||||||
|
icon-translations: done
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues:
|
||||||
|
status: exempt
|
||||||
|
comment: No known use cases for repair issues or flows, yet
|
||||||
|
stale-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Device type integration
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: done
|
249
homeassistant/components/altruist/sensor.py
Normal file
249
homeassistant/components/altruist/sensor.py
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
"""Defines the Altruist sensor platform."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
PERCENTAGE,
|
||||||
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
|
EntityCategory,
|
||||||
|
UnitOfPressure,
|
||||||
|
UnitOfSoundPressure,
|
||||||
|
UnitOfTemperature,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from . import AltruistConfigEntry
|
||||||
|
from .coordinator import AltruistDataUpdateCoordinator
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AltruistSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Class to describe a Sensor entity."""
|
||||||
|
|
||||||
|
native_value_fn: Callable[[str], float] = float
|
||||||
|
state_class = SensorStateClass.MEASUREMENT
|
||||||
|
|
||||||
|
|
||||||
|
SENSOR_DESCRIPTIONS = [
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
key="BME280_humidity",
|
||||||
|
translation_key="humidity",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "BME280"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
key="BME280_pressure",
|
||||||
|
translation_key="pressure",
|
||||||
|
native_unit_of_measurement=UnitOfPressure.PA,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
translation_placeholders={"sensor_name": "BME280"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
key="BME280_temperature",
|
||||||
|
translation_key="temperature",
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "BME280"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
key="BMP_pressure",
|
||||||
|
translation_key="pressure",
|
||||||
|
native_unit_of_measurement=UnitOfPressure.PA,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
translation_placeholders={"sensor_name": "BMP"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
key="BMP_temperature",
|
||||||
|
translation_key="temperature",
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "BMP"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
key="BMP280_temperature",
|
||||||
|
translation_key="temperature",
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "BMP280"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
key="BMP280_pressure",
|
||||||
|
translation_key="pressure",
|
||||||
|
native_unit_of_measurement=UnitOfPressure.PA,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
translation_placeholders={"sensor_name": "BMP280"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
key="HTU21D_humidity",
|
||||||
|
translation_key="humidity",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "HTU21D"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
key="HTU21D_temperature",
|
||||||
|
translation_key="temperature",
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "HTU21D"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.PM10,
|
||||||
|
translation_key="pm_10",
|
||||||
|
key="SDS_P1",
|
||||||
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.PM25,
|
||||||
|
translation_key="pm_25",
|
||||||
|
key="SDS_P2",
|
||||||
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
key="SHT3X_humidity",
|
||||||
|
translation_key="humidity",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "SHT3X"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
key="SHT3X_temperature",
|
||||||
|
translation_key="temperature",
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "SHT3X"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
|
key="signal",
|
||||||
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||||
|
key="PCBA_noiseMax",
|
||||||
|
translation_key="noise_max",
|
||||||
|
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||||
|
key="PCBA_noiseAvg",
|
||||||
|
translation_key="noise_avg",
|
||||||
|
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.CO2,
|
||||||
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
translation_key="co2",
|
||||||
|
key="CCS_CO2",
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "CCS"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||||
|
key="CCS_TVOC",
|
||||||
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
key="GC",
|
||||||
|
native_unit_of_measurement="μR/h",
|
||||||
|
translation_key="radiation",
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.CO2,
|
||||||
|
translation_key="co2",
|
||||||
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
key="SCD4x_co2",
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "SCD4x"},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AltruistConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Add sensors for passed config_entry in HA."""
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
async_add_entities(
|
||||||
|
AltruistSensor(coordinator, sensor_description)
|
||||||
|
for sensor_description in SENSOR_DESCRIPTIONS
|
||||||
|
if sensor_description.key in coordinator.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AltruistSensor(CoordinatorEntity[AltruistDataUpdateCoordinator], SensorEntity):
|
||||||
|
"""Implementation of a Altruist sensor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AltruistDataUpdateCoordinator,
|
||||||
|
description: AltruistSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Altruist sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._device = coordinator.client.device
|
||||||
|
self.entity_description: AltruistSensorEntityDescription = description
|
||||||
|
self._attr_unique_id = f"{self._device.id}-{description.key}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
connections={(CONNECTION_NETWORK_MAC, self._device.id)},
|
||||||
|
manufacturer="Robonomics",
|
||||||
|
model="Altruist",
|
||||||
|
sw_version=self._device.fw_version,
|
||||||
|
configuration_url=f"http://{self._device.ip_address}",
|
||||||
|
serial_number=self._device.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return (
|
||||||
|
super().available and self.entity_description.key in self.coordinator.data
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float | int:
|
||||||
|
"""Return the native value of the sensor."""
|
||||||
|
string_value = self.coordinator.data[self.entity_description.key]
|
||||||
|
return self.entity_description.native_value_fn(string_value)
|
51
homeassistant/components/altruist/strings.json
Normal file
51
homeassistant/components/altruist/strings.json
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "{name}",
|
||||||
|
"step": {
|
||||||
|
"discovery_confirm": {
|
||||||
|
"description": "Do you want to start setup {model}?"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"host": "Altruist IP address or hostname in the local network"
|
||||||
|
},
|
||||||
|
"description": "Fill in Altruist IP address or hostname in your local network"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_device_found": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"humidity": {
|
||||||
|
"name": "{sensor_name} humidity"
|
||||||
|
},
|
||||||
|
"pressure": {
|
||||||
|
"name": "{sensor_name} pressure"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"name": "{sensor_name} temperature"
|
||||||
|
},
|
||||||
|
"noise_max": {
|
||||||
|
"name": "Maximum noise"
|
||||||
|
},
|
||||||
|
"noise_avg": {
|
||||||
|
"name": "Average noise"
|
||||||
|
},
|
||||||
|
"co2": {
|
||||||
|
"name": "{sensor_name} CO2"
|
||||||
|
},
|
||||||
|
"radiation": {
|
||||||
|
"name": "Radiation level"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@ -48,6 +48,7 @@ FLOWS = {
|
|||||||
"airzone_cloud",
|
"airzone_cloud",
|
||||||
"alarmdecoder",
|
"alarmdecoder",
|
||||||
"alexa_devices",
|
"alexa_devices",
|
||||||
|
"altruist",
|
||||||
"amberelectric",
|
"amberelectric",
|
||||||
"ambient_network",
|
"ambient_network",
|
||||||
"ambient_station",
|
"ambient_station",
|
||||||
|
@ -204,6 +204,12 @@
|
|||||||
"config_flow": false,
|
"config_flow": false,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
},
|
},
|
||||||
|
"altruist": {
|
||||||
|
"name": "Altruist",
|
||||||
|
"integration_type": "device",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "local_polling"
|
||||||
|
},
|
||||||
"amazon": {
|
"amazon": {
|
||||||
"name": "Amazon",
|
"name": "Amazon",
|
||||||
"integrations": {
|
"integrations": {
|
||||||
|
5
homeassistant/generated/zeroconf.py
generated
5
homeassistant/generated/zeroconf.py
generated
@ -342,6 +342,11 @@ ZEROCONF = {
|
|||||||
"domain": "apple_tv",
|
"domain": "apple_tv",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"_altruist._tcp.local.": [
|
||||||
|
{
|
||||||
|
"domain": "altruist",
|
||||||
|
},
|
||||||
|
],
|
||||||
"_amzn-alexa._tcp.local.": [
|
"_amzn-alexa._tcp.local.": [
|
||||||
{
|
{
|
||||||
"domain": "roomba",
|
"domain": "roomba",
|
||||||
|
10
mypy.ini
generated
10
mypy.ini
generated
@ -425,6 +425,16 @@ disallow_untyped_defs = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.altruist.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.amazon_polly.*]
|
[mypy-homeassistant.components.amazon_polly.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@ -461,6 +461,9 @@ airtouch5py==0.3.0
|
|||||||
# homeassistant.components.alpha_vantage
|
# homeassistant.components.alpha_vantage
|
||||||
alpha-vantage==2.3.1
|
alpha-vantage==2.3.1
|
||||||
|
|
||||||
|
# homeassistant.components.altruist
|
||||||
|
altruistclient==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.amberelectric
|
# homeassistant.components.amberelectric
|
||||||
amberelectric==2.0.12
|
amberelectric==2.0.12
|
||||||
|
|
||||||
|
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@ -440,6 +440,9 @@ airtouch4pyapi==1.0.5
|
|||||||
# homeassistant.components.airtouch5
|
# homeassistant.components.airtouch5
|
||||||
airtouch5py==0.3.0
|
airtouch5py==0.3.0
|
||||||
|
|
||||||
|
# homeassistant.components.altruist
|
||||||
|
altruistclient==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.amberelectric
|
# homeassistant.components.amberelectric
|
||||||
amberelectric==2.0.12
|
amberelectric==2.0.12
|
||||||
|
|
||||||
|
13
tests/components/altruist/__init__.py
Normal file
13
tests/components/altruist/__init__.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"""Tests for the Altruist integration."""
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||||
|
"""Fixture for setting up the component."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
82
tests/components/altruist/conftest.py
Normal file
82
tests/components/altruist/conftest.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
"""Altruist tests configuration."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
import json
|
||||||
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
|
from altruistclient import AltruistDeviceModel, AltruistError
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.altruist.const import CONF_HOST, DOMAIN
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_fixture
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||||
|
"""Override async_setup_entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.altruist.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""Return a mock config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={CONF_HOST: "192.168.1.100"},
|
||||||
|
unique_id="5366960e8b18",
|
||||||
|
title="5366960e8b18",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_altruist_device() -> Mock:
|
||||||
|
"""Return a mock AltruistDeviceModel."""
|
||||||
|
device = Mock(spec=AltruistDeviceModel)
|
||||||
|
device.id = "5366960e8b18"
|
||||||
|
device.name = "Altruist Sensor"
|
||||||
|
device.ip_address = "192.168.1.100"
|
||||||
|
device.fw_version = "R_2025-03"
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_altruist_client(mock_altruist_device: Mock) -> Generator[AsyncMock]:
|
||||||
|
"""Return a mock AltruistClient."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.altruist.coordinator.AltruistClient",
|
||||||
|
autospec=True,
|
||||||
|
) as mock_client_class,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.altruist.config_flow.AltruistClient",
|
||||||
|
new=mock_client_class,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
mock_instance = AsyncMock()
|
||||||
|
mock_instance.device = mock_altruist_device
|
||||||
|
mock_instance.device_id = mock_altruist_device.id
|
||||||
|
mock_instance.sensor_names = json.loads(
|
||||||
|
load_fixture("sensor_names.json", DOMAIN)
|
||||||
|
)
|
||||||
|
mock_instance.fetch_data.return_value = json.loads(
|
||||||
|
load_fixture("real_data.json", DOMAIN)
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_client_class.from_ip_address = AsyncMock(return_value=mock_instance)
|
||||||
|
|
||||||
|
yield mock_instance
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_altruist_client_fails_once(mock_altruist_client: AsyncMock) -> Generator[None]:
|
||||||
|
"""Patch AltruistClient to fail once and then succeed."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.altruist.config_flow.AltruistClient.from_ip_address",
|
||||||
|
side_effect=[AltruistError("Connection failed"), mock_altruist_client],
|
||||||
|
):
|
||||||
|
yield
|
38
tests/components/altruist/fixtures/real_data.json
Normal file
38
tests/components/altruist/fixtures/real_data.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"value_type": "signal",
|
||||||
|
"value": "-48"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "SDS_P1",
|
||||||
|
"value": "0.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "SDS_P2",
|
||||||
|
"value": "0.23"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "BME280_humidity",
|
||||||
|
"value": "54.94141"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "BME280_temperature",
|
||||||
|
"value": "22.95313"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "BME280_pressure",
|
||||||
|
"value": "99978.16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "PCBA_noiseMax",
|
||||||
|
"value": "60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "PCBA_noiseAvg",
|
||||||
|
"value": "51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value_type": "GC",
|
||||||
|
"value": "15.2"
|
||||||
|
}
|
||||||
|
]
|
11
tests/components/altruist/fixtures/sensor_names.json
Normal file
11
tests/components/altruist/fixtures/sensor_names.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[
|
||||||
|
"signal",
|
||||||
|
"SDS_P1",
|
||||||
|
"SDS_P2",
|
||||||
|
"BME280_humidity",
|
||||||
|
"BME280_temperature",
|
||||||
|
"BME280_pressure",
|
||||||
|
"PCBA_noiseMax",
|
||||||
|
"PCBA_noiseAvg",
|
||||||
|
"GC"
|
||||||
|
]
|
507
tests/components/altruist/snapshots/test_sensor.ambr
Normal file
507
tests/components/altruist/snapshots/test_sensor.ambr
Normal file
@ -0,0 +1,507 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_average_noise-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_average_noise',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.SOUND_PRESSURE: 'sound_pressure'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Average noise',
|
||||||
|
'platform': 'altruist',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'noise_avg',
|
||||||
|
'unique_id': '5366960e8b18-PCBA_noiseAvg',
|
||||||
|
'unit_of_measurement': <UnitOfSoundPressure.DECIBEL: 'dB'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_average_noise-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'sound_pressure',
|
||||||
|
'friendly_name': '5366960e8b18 Average noise',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': <UnitOfSoundPressure.DECIBEL: 'dB'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_average_noise',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '51.0',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_bme280_humidity-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_bme280_humidity',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'BME280 humidity',
|
||||||
|
'platform': 'altruist',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'humidity',
|
||||||
|
'unique_id': '5366960e8b18-BME280_humidity',
|
||||||
|
'unit_of_measurement': '%',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_bme280_humidity-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'humidity',
|
||||||
|
'friendly_name': '5366960e8b18 BME280 humidity',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': '%',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_bme280_humidity',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '54.94141',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_bme280_pressure-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_bme280_pressure',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 0,
|
||||||
|
}),
|
||||||
|
'sensor.private': dict({
|
||||||
|
'suggested_unit_of_measurement': <UnitOfPressure.MMHG: 'mmHg'>,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.PRESSURE: 'pressure'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'BME280 pressure',
|
||||||
|
'platform': 'altruist',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'pressure',
|
||||||
|
'unique_id': '5366960e8b18-BME280_pressure',
|
||||||
|
'unit_of_measurement': <UnitOfPressure.MMHG: 'mmHg'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_bme280_pressure-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'pressure',
|
||||||
|
'friendly_name': '5366960e8b18 BME280 pressure',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': <UnitOfPressure.MMHG: 'mmHg'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_bme280_pressure',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '749.897762397492',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_bme280_temperature-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_bme280_temperature',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'BME280 temperature',
|
||||||
|
'platform': 'altruist',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'temperature',
|
||||||
|
'unique_id': '5366960e8b18-BME280_temperature',
|
||||||
|
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_bme280_temperature-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'temperature',
|
||||||
|
'friendly_name': '5366960e8b18 BME280 temperature',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_bme280_temperature',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '22.95313',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_maximum_noise-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_maximum_noise',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.SOUND_PRESSURE: 'sound_pressure'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Maximum noise',
|
||||||
|
'platform': 'altruist',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'noise_max',
|
||||||
|
'unique_id': '5366960e8b18-PCBA_noiseMax',
|
||||||
|
'unit_of_measurement': <UnitOfSoundPressure.DECIBEL: 'dB'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_maximum_noise-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'sound_pressure',
|
||||||
|
'friendly_name': '5366960e8b18 Maximum noise',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': <UnitOfSoundPressure.DECIBEL: 'dB'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_maximum_noise',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '60.0',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_pm10-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_pm10',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.PM10: 'pm10'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'PM10',
|
||||||
|
'platform': 'altruist',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'pm_10',
|
||||||
|
'unique_id': '5366960e8b18-SDS_P1',
|
||||||
|
'unit_of_measurement': 'µg/m³',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_pm10-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'pm10',
|
||||||
|
'friendly_name': '5366960e8b18 PM10',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': 'µg/m³',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_pm10',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '0.1',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_pm2_5-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_pm2_5',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.PM25: 'pm25'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'PM2.5',
|
||||||
|
'platform': 'altruist',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'pm_25',
|
||||||
|
'unique_id': '5366960e8b18-SDS_P2',
|
||||||
|
'unit_of_measurement': 'µg/m³',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_pm2_5-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'pm25',
|
||||||
|
'friendly_name': '5366960e8b18 PM2.5',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': 'µg/m³',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_pm2_5',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '0.23',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_radiation_level-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_radiation_level',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Radiation level',
|
||||||
|
'platform': 'altruist',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'radiation',
|
||||||
|
'unique_id': '5366960e8b18-GC',
|
||||||
|
'unit_of_measurement': 'μR/h',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_radiation_level-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': '5366960e8b18 Radiation level',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': 'μR/h',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_radiation_level',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '15.2',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_signal_strength-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_signal_strength',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Signal strength',
|
||||||
|
'platform': 'altruist',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': None,
|
||||||
|
'unique_id': '5366960e8b18-signal',
|
||||||
|
'unit_of_measurement': 'dBm',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[sensor.5366960e8b18_signal_strength-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'signal_strength',
|
||||||
|
'friendly_name': '5366960e8b18 Signal strength',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': 'dBm',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.5366960e8b18_signal_strength',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '-48.0',
|
||||||
|
})
|
||||||
|
# ---
|
169
tests/components/altruist/test_config_flow.py
Normal file
169
tests/components/altruist/test_config_flow.py
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
"""Test the Altruist config flow."""
|
||||||
|
|
||||||
|
from ipaddress import ip_address
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from homeassistant.components.altruist.const import CONF_HOST, DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||||
|
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
|
||||||
|
|
||||||
|
ZEROCONF_DISCOVERY = ZeroconfServiceInfo(
|
||||||
|
ip_address=ip_address("192.168.1.100"),
|
||||||
|
ip_addresses=[ip_address("192.168.1.100")],
|
||||||
|
hostname="altruist-purple.local.",
|
||||||
|
name="altruist-purple._altruist._tcp.local.",
|
||||||
|
port=80,
|
||||||
|
type="_altruist._tcp.local.",
|
||||||
|
properties={
|
||||||
|
"PATH": "/config",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_user_step_success(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_altruist_client: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test user step shows form and succeeds with valid input."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_HOST: "192.168.1.100"},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "5366960e8b18"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_HOST: "192.168.1.100",
|
||||||
|
}
|
||||||
|
assert result["result"].unique_id == "5366960e8b18"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_user_step_cannot_connect_then_recovers(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_altruist_client: AsyncMock,
|
||||||
|
mock_altruist_client_fails_once: None,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test we handle connection error and allow recovery."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
# First attempt triggers an error
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_HOST: "192.168.1.100"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": "no_device_found"}
|
||||||
|
|
||||||
|
# Second attempt recovers with a valid client
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_HOST: "192.168.1.100"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "5366960e8b18"
|
||||||
|
assert result["result"].unique_id == "5366960e8b18"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_HOST: "192.168.1.100",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_user_step_already_configured(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_altruist_client: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test we abort if already configured."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
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: "192.168.1.100"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_zeroconf_discovery(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_altruist_client: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test zeroconf discovery."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_ZEROCONF},
|
||||||
|
data=ZEROCONF_DISCOVERY,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "discovery_confirm"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "5366960e8b18"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_HOST: "192.168.1.100",
|
||||||
|
}
|
||||||
|
assert result["result"].unique_id == "5366960e8b18"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_zeroconf_discovery_already_configured(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_altruist_client: AsyncMock,
|
||||||
|
mock_setup_entry: 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=ZEROCONF_DISCOVERY,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_zeroconf_discovery_cant_create_client(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_altruist_client_fails_once: AsyncMock,
|
||||||
|
mock_setup_entry: 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=ZEROCONF_DISCOVERY,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "no_device_found"
|
53
tests/components/altruist/test_init.py
Normal file
53
tests/components/altruist/test_init.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"""Test the Altruist integration."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_entry_client_creation_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_altruist_client_fails_once: None,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup failure when client creation fails."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert not await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_entry_fetch_data_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_altruist_client: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup failure when initial data fetch fails."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
mock_altruist_client.fetch_data.side_effect = Exception("Fetch failed")
|
||||||
|
|
||||||
|
assert not await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unload_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_altruist_client: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test unloading of config entry."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
# Now test unloading
|
||||||
|
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
55
tests/components/altruist/test_sensor.py
Normal file
55
tests/components/altruist/test_sensor.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
"""Tests for the Altruist integration sensor platform."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from altruistclient import AltruistError
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_all_entities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
mock_altruist_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test all entities."""
|
||||||
|
with patch("homeassistant.components.altruist.PLATFORMS", [Platform.SENSOR]):
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_connection_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_altruist_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test coordinator error handling during update."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
mock_altruist_client.fetch_data.side_effect = AltruistError()
|
||||||
|
|
||||||
|
freezer.tick(timedelta(minutes=1))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
hass.states.get("sensor.5366960e8b18_bme280_temperature").state
|
||||||
|
== STATE_UNAVAILABLE
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user