Compare commits

..

26 Commits

Author SHA1 Message Date
A. Gideonse 7e2343243d Add charge/discharge remaining time to Indevolt (#174600) 2026-06-26 16:42:47 +02:00
Simone Chemelli de8ed15ea8 Handle all login exceptions in Vodafone Station (#174852)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-06-26 16:34:52 +02:00
Allen Porter 9a21d92908 Bump voluptuous-openapi to 0.4.1 (#174912) 2026-06-26 16:12:28 +02:00
GSzabados 12ea0c4d77 Update units UnitOfDensity / UnitOfRatio for airnow (#174905) 2026-06-26 14:57:50 +02:00
prana-dev-official 94c4483735 Update units UnitOfRatio enums for Prana (#174907)
Co-authored-by: yuriipopow <yurapopov522@gmail.com>
2026-06-26 14:23:04 +02:00
GSzabados 053efdf662 Update units UnitOfDensity / UnitOfRatio for airthings_ble (#174906) 2026-06-26 14:16:49 +02:00
GSzabados 3c9f55f7b2 Update units UnitOfDensity / UnitOfRatio for ambient_station (#174900) 2026-06-26 12:53:23 +02:00
GSzabados a46b434930 Update measurement units UnitOfDensity / UnitOfRatio for Tomorrow.io (#174899) 2026-06-26 12:42:49 +02:00
Renat Sibgatulin 031e764957 Migrate the airq to the new enums (UnitOfDensity / UnitOfRatio) (#174896) 2026-06-26 12:26:52 +02:00
Paulus Schoutsen d9f0faf365 Fix Roborock time entity crash when timer value is missing (#174873)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-26 06:25:55 -04:00
Joost Lekkerkerker a14e5d8a0c Change Airthings BLE level entities in enum device class (#174815) 2026-06-26 12:14:49 +02:00
Manu f356a1cd0d Remove ThermoWorks Smoke (#174845) 2026-06-26 11:39:56 +02:00
starkillerOG 50c12d85f8 Add Reolink push command IDs (#174876) 2026-06-26 11:38:48 +02:00
javicalle a88b43d845 Migrate rflink to UnitOfRatio enum (#174886) 2026-06-26 11:21:21 +02:00
GSzabados b9e59522e3 Update measurement units UnitOfDensity / UnitOfRatio for EcoWitt sensors (#174887) 2026-06-26 11:19:21 +02:00
Ludovic BOUÉ 1484384d63 Bump roborock dependencies to 5.21.0 (#174841) 2026-06-26 11:06:00 +02:00
Paulus Schoutsen 1d1ab798df Fix Roborock number entity crash when volume is None (#174872)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-26 11:05:12 +02:00
Erwin Douna 926d2f1e21 Shelly refactor UnitsOf (#174883) 2026-06-26 11:01:23 +02:00
dependabot[bot] b341228b4b Bump actions/checkout from 6.0.3 to 7.0.0 (#174875)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-26 10:58:47 +02:00
renovate[bot] 241f850e90 Update pytest-github-actions-annotate-failures to 0.4.1 (#174868) 2026-06-26 10:34:57 +02:00
Rafa PA 257040ac51 [aemet] Increase weather update interval to 20 minutes (#174803) 2026-06-26 10:01:09 +02:00
Arie Catsman e3c17026d0 Update enphase_envoy diagnostics for pyenphase lib v3.0.0 (#174524) 2026-06-26 10:00:35 +02:00
Stefan Agner 9e4550dd14 Fix hassio job subscribe returning None instead of unsubscribe callback (#174063)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-26 09:56:50 +02:00
starkillerOG 37f441d3da Bump reolink_aio to 0.21.3 (#174879) 2026-06-26 09:53:04 +02:00
Maciej Bieniek 0099100d14 Use UnitOfDensity enum in GIOS (#174855) 2026-06-26 07:59:40 +02:00
Maciej Bieniek e20f74dac5 Use new unit enums in NAM (#174856) 2026-06-26 07:59:37 +02:00
65 changed files with 919 additions and 786 deletions
+4 -4
View File
@@ -31,7 +31,7 @@
# - GITHUB_TOKEN
#
# Custom actions used:
# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -155,7 +155,7 @@ jobs:
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
- name: Checkout .github and .agents folders
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
sparse-checkout: |
@@ -403,7 +403,7 @@ jobs:
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
} >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Create gh-aw temp directory
@@ -1234,7 +1234,7 @@ jobs:
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
- name: Checkout repository for patch context
if: needs.agent.outputs.has_patch == 'true'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# --- Threat Detection ---
@@ -27,7 +27,7 @@ from .const import CONDITIONS_MAP, DOMAIN, FORECAST_MAP
_LOGGER = logging.getLogger(__name__)
API_TIMEOUT: Final[int] = 120
WEATHER_UPDATE_INTERVAL = timedelta(minutes=10)
WEATHER_UPDATE_INTERVAL = timedelta(minutes=20)
type AemetConfigEntry = ConfigEntry[AemetData]
+4 -8
View File
@@ -12,11 +12,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
ATTR_TIME,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
)
from homeassistant.const import ATTR_TIME, UnitOfDensity, UnitOfRatio
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -95,7 +91,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription(
key=ATTR_API_PM10,
translation_key="pm10",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PM10,
value_fn=lambda data: data.get(ATTR_API_PM10),
@@ -104,7 +100,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription(
key=ATTR_API_PM25,
translation_key="pm25",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PM25,
value_fn=lambda data: data.get(ATTR_API_PM25),
@@ -113,7 +109,7 @@ SENSOR_TYPES: tuple[AirNowEntityDescription, ...] = (
AirNowEntityDescription(
key=ATTR_API_O3,
translation_key="o3",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.get(ATTR_API_O3),
extra_state_attributes_fn=None,
+2 -2
View File
@@ -8,7 +8,7 @@ from typing import override
from aioairq.core import AirQ
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.const import PERCENTAGE
from homeassistant.const import UnitOfRatio
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -32,7 +32,7 @@ AIRQ_LED_BRIGHTNESS = AirQBrightnessDescription(
native_min_value=0.0,
native_max_value=100.0,
native_step=1.0,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
value=lambda data: data["brightness"],
set_value=lambda device, value: device.set_current_brightness(value),
)
+47 -51
View File
@@ -12,13 +12,9 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
UnitOfDensity,
UnitOfPressure,
UnitOfRatio,
UnitOfSoundPressure,
UnitOfTemperature,
)
@@ -44,70 +40,70 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
AirQEntityDescription(
key="c2h4o",
translation_key="acetaldehyde",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("c2h4o"),
),
AirQEntityDescription(
key="nh3_MR100",
translation_key="ammonia",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("nh3_MR100"),
),
AirQEntityDescription(
key="ash3",
translation_key="arsine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ash3"),
),
AirQEntityDescription(
key="br2",
translation_key="bromine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("br2"),
),
AirQEntityDescription(
key="ch4s",
translation_key="methanethiol",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ch4s"),
),
AirQEntityDescription(
key="cl2_M20",
translation_key="chlorine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("cl2_M20"),
),
AirQEntityDescription(
key="clo2",
translation_key="chlorine_dioxide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("clo2"),
),
AirQEntityDescription(
key="co",
translation_key="carbon_monoxide",
native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("co"),
),
AirQEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("co2"),
),
AirQEntityDescription(
key="cs2",
translation_key="carbon_disulfide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("cs2"),
),
@@ -122,182 +118,182 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
AirQEntityDescription(
key="ethanol",
translation_key="ethanol",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ethanol"),
),
AirQEntityDescription(
key="c2h4",
translation_key="ethylene",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("c2h4"),
),
AirQEntityDescription(
key="ch2o_M10",
translation_key="formaldehyde",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ch2o_M10"),
),
AirQEntityDescription(
key="f2",
translation_key="fluorine",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("f2"),
),
AirQEntityDescription(
key="h2s",
translation_key="hydrogen_sulfide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("h2s"),
),
AirQEntityDescription(
key="hcl",
translation_key="hydrochloric_acid",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("hcl"),
),
AirQEntityDescription(
key="hcn",
translation_key="hydrogen_cyanide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("hcn"),
),
AirQEntityDescription(
key="hf",
translation_key="hydrogen_fluoride",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("hf"),
),
AirQEntityDescription(
key="health",
translation_key="health_index",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("health", 0.0) / 10.0,
),
AirQEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("humidity"),
),
AirQEntityDescription(
key="humidity_abs",
device_class=SensorDeviceClass.ABSOLUTE_HUMIDITY,
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.GRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("humidity_abs"),
),
AirQEntityDescription(
key="h2_M1000",
translation_key="hydrogen",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("h2_M1000"),
),
AirQEntityDescription(
key="h2o2",
translation_key="hydrogen_peroxide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("h2o2"),
),
AirQEntityDescription(
key="ch4_MIPEX",
translation_key="methane",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ch4_MIPEX"),
),
AirQEntityDescription(
key="mold",
translation_key="mold",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("mold"),
),
AirQEntityDescription(
key="n2o",
device_class=SensorDeviceClass.NITROUS_OXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("n2o"),
),
AirQEntityDescription(
key="no_M250",
device_class=SensorDeviceClass.NITROGEN_MONOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("no_M250"),
),
AirQEntityDescription(
key="no2",
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("no2"),
),
AirQEntityDescription(
key="acid_M100",
translation_key="organic_acid",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("acid_M100"),
),
AirQEntityDescription(
key="oxygen",
translation_key="oxygen",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("oxygen"),
),
AirQEntityDescription(
key="o3",
device_class=SensorDeviceClass.OZONE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("o3"),
),
AirQEntityDescription(
key="performance",
translation_key="performance_index",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("performance", 0.0) / 10.0,
),
AirQEntityDescription(
key="ph3",
translation_key="hydrogen_phosphide",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("ph3"),
),
AirQEntityDescription(
key="pm1",
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pm1"),
),
AirQEntityDescription(
key="pm2_5",
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pm2_5"),
),
AirQEntityDescription(
key="pm10",
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("pm10"),
),
@@ -319,42 +315,42 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
AirQEntityDescription(
key="c3h8_MIPEX",
translation_key="propane",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("c3h8_MIPEX"),
),
AirQEntityDescription(
key="r32",
translation_key="r32",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("r32"),
),
AirQEntityDescription(
key="r454b",
translation_key="r454b",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("r454b"),
),
AirQEntityDescription(
key="r454c",
translation_key="r454c",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("r454c"),
),
AirQEntityDescription(
key="sih4",
translation_key="silicon_hydride",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("sih4"),
),
AirQEntityDescription(
key="so2",
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("so2"),
),
@@ -391,7 +387,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
AirQEntityDescription(
key="tvoc",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("tvoc"),
),
@@ -399,14 +395,14 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
key="tvoc_ionsc",
translation_key="industrial_volatile_organic_compounds",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("tvoc_ionsc"),
),
AirQEntityDescription(
key="virus",
translation_key="virus_index",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value=lambda data: data.get("virus", 0.0),
),
@@ -1,6 +1,8 @@
"""Support for airthings ble sensors."""
from collections.abc import Callable
import dataclasses
from dataclasses import dataclass
import logging
from typing import override
@@ -13,13 +15,11 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
EntityCategory,
Platform,
UnitOfPressure,
UnitOfRatio,
UnitOfSoundPressure,
UnitOfTemperature,
)
@@ -46,87 +46,108 @@ CONNECTIVITY_MODE_MAP = {
AirthingsConnectivityMode.NOT_CONFIGURED.value: "not_configured",
}
SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
"radon_1day_avg": SensorEntityDescription(
def get_connectivity_mode(value: str | float | None) -> str | None:
"""Get connectivity mode."""
if not isinstance(value, str):
return None
return CONNECTIVITY_MODE_MAP.get(value)
@dataclass(frozen=True, kw_only=True)
class AirthingsBLESensorEntityDescription(SensorEntityDescription):
"""Describes Airthings BLE sensor entity."""
value_fn: Callable[[str | float | None], str | float | None] = lambda x: x
SENSORS_MAPPING_TEMPLATE: dict[str, AirthingsBLESensorEntityDescription] = {
"radon_1day_avg": AirthingsBLESensorEntityDescription(
key="radon_1day_avg",
translation_key="radon_1day_avg",
native_unit_of_measurement=VOLUME_BECQUEREL,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
),
"radon_longterm_avg": SensorEntityDescription(
"radon_longterm_avg": AirthingsBLESensorEntityDescription(
key="radon_longterm_avg",
translation_key="radon_longterm_avg",
native_unit_of_measurement=VOLUME_BECQUEREL,
suggested_display_precision=0,
state_class=SensorStateClass.MEASUREMENT,
),
"radon_1day_level": SensorEntityDescription(
"radon_1day_level": AirthingsBLESensorEntityDescription(
key="radon_1day_level",
translation_key="radon_1day_level",
device_class=SensorDeviceClass.ENUM,
options=["good", "fair", "poor"],
value_fn=lambda value: value if value != "unknown" else None,
),
"radon_longterm_level": SensorEntityDescription(
"radon_longterm_level": AirthingsBLESensorEntityDescription(
key="radon_longterm_level",
translation_key="radon_longterm_level",
device_class=SensorDeviceClass.ENUM,
options=["good", "fair", "poor"],
value_fn=lambda value: value if value != "unknown" else None,
),
"temperature": SensorEntityDescription(
"temperature": AirthingsBLESensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
"humidity": SensorEntityDescription(
"humidity": AirthingsBLESensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
"pressure": SensorEntityDescription(
"pressure": AirthingsBLESensorEntityDescription(
key="pressure",
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
native_unit_of_measurement=UnitOfPressure.MBAR,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
"battery": SensorEntityDescription(
"battery": AirthingsBLESensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=0,
),
"co2": SensorEntityDescription(
"co2": AirthingsBLESensorEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"voc": SensorEntityDescription(
"voc": AirthingsBLESensorEntityDescription(
key="voc",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"illuminance": SensorEntityDescription(
"illuminance": AirthingsBLESensorEntityDescription(
key="illuminance",
translation_key="illuminance",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"lux": SensorEntityDescription(
"lux": AirthingsBLESensorEntityDescription(
key="lux",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"noise": SensorEntityDescription(
"noise": AirthingsBLESensorEntityDescription(
key="noise",
translation_key="ambient_noise",
device_class=SensorDeviceClass.SOUND_PRESSURE,
@@ -134,13 +155,14 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"connectivity_mode": SensorEntityDescription(
"connectivity_mode": AirthingsBLESensorEntityDescription(
key="connectivity_mode",
translation_key="connectivity_mode",
device_class=SensorDeviceClass.ENUM,
options=list(CONNECTIVITY_MODE_MAP.values()),
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=get_connectivity_mode,
),
}
@@ -228,12 +250,13 @@ class AirthingsSensor(
"""Airthings BLE sensors for the device."""
_attr_has_entity_name = True
entity_description: AirthingsBLESensorEntityDescription
def __init__(
self,
coordinator: AirthingsBLEDataUpdateCoordinator,
airthings_device: AirthingsDevice,
entity_description: SensorEntityDescription,
entity_description: AirthingsBLESensorEntityDescription,
) -> None:
"""Populate the airthings entity with relevant data."""
super().__init__(coordinator)
@@ -272,11 +295,4 @@ class AirthingsSensor(
def native_value(self) -> StateType:
"""Return the value reported by the sensor."""
value = self.coordinator.data.sensors[self.entity_description.key]
# Map connectivity mode to enum values
if self.entity_description.key == "connectivity_mode":
if not isinstance(value, str):
return None
return CONNECTIVITY_MODE_MAP.get(value)
return value
return self.entity_description.value_fn(value)
@@ -45,13 +45,23 @@
"name": "Radon 1-day average"
},
"radon_1day_level": {
"name": "Radon 1-day level"
"name": "Radon 1-day level",
"state": {
"fair": "Fair",
"good": "Good",
"poor": "Poor"
}
},
"radon_longterm_avg": {
"name": "Radon longterm average"
},
"radon_longterm_level": {
"name": "Radon longterm level"
"name": "Radon longterm level",
"state": {
"fair": "[%key:component::airthings_ble::entity::sensor::radon_1day_level::state::fair%]",
"good": "[%key:component::airthings_ble::entity::sensor::radon_1day_level::state::good%]",
"poor": "[%key:component::airthings_ble::entity::sensor::radon_1day_level::state::poor%]"
}
}
}
},
@@ -11,15 +11,14 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import (
ATTR_NAME,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
UnitOfDensity,
UnitOfIrradiance,
UnitOfLength,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfRatio,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolumetricFlux,
@@ -157,13 +156,13 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_PM25_IN_AQIN,
translation_key="pm25_indoor_aqin",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_PM25_IN_24H_AQIN,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
translation_key="pm25_indoor_24h_aqin",
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
@@ -171,28 +170,28 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_PM10_IN_AQIN,
translation_key="pm10_indoor_aqin",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_PM10_IN_24H_AQIN,
translation_key="pm10_indoor_24h_aqin",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_CO2_IN_AQIN,
translation_key="co2_indoor_aqin",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_CO2_IN_24H_AQIN,
translation_key="co2_indoor_24h_aqin",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -206,7 +205,7 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_PM_IN_HUMIDITY_AQIN,
translation_key="pm_indoor_humidity_aqin",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -250,7 +249,7 @@ SENSOR_DESCRIPTIONS = (
),
SensorEntityDescription(
key=TYPE_CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -291,83 +290,83 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_HUMIDITY10,
translation_key="humidity_10",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY1,
translation_key="humidity_1",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY2,
translation_key="humidity_2",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY3,
translation_key="humidity_3",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY4,
translation_key="humidity_4",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY5,
translation_key="humidity_5",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY6,
translation_key="humidity_6",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY7,
translation_key="humidity_7",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY8,
translation_key="humidity_8",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY9,
translation_key="humidity_9",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_HUMIDITYIN,
translation_key="humidity_indoor",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -417,95 +416,95 @@ SENSOR_DESCRIPTIONS = (
SensorEntityDescription(
key=TYPE_PM25_24H,
translation_key="pm25_24h_average",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
),
SensorEntityDescription(
key=TYPE_PM25_IN,
translation_key="pm25_indoor",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_PM25_IN_24H,
translation_key="pm25_indoor_24h_average",
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
),
SensorEntityDescription(
key=TYPE_PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM10,
translation_key="soil_humidity_10",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM1,
translation_key="soil_humidity_1",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM2,
translation_key="soil_humidity_2",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM3,
translation_key="soil_humidity_3",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM4,
translation_key="soil_humidity_4",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM5,
translation_key="soil_humidity_5",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM6,
translation_key="soil_humidity_6",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM7,
translation_key="soil_humidity_7",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM8,
translation_key="soil_humidity_8",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key=TYPE_SOILHUM9,
translation_key="soil_humidity_9",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
+11 -12
View File
@@ -14,18 +14,17 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
UV_INDEX,
EntityCategory,
UnitOfDensity,
UnitOfElectricPotential,
UnitOfIrradiance,
UnitOfLength,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfRatio,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolumetricFlux,
@@ -99,7 +98,7 @@ ECOWITT_SENSORS_MAPPING: Final = {
EcoWittSensorTypes.HUMIDITY: SensorEntityDescription(
key="HUMIDITY",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.DEGREE: SensorEntityDescription(
@@ -122,19 +121,19 @@ ECOWITT_SENSORS_MAPPING: Final = {
EcoWittSensorTypes.PM25: SensorEntityDescription(
key="PM25",
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.PM10: SensorEntityDescription(
key="PM10",
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.BATTERY_PERCENTAGE: SensorEntityDescription(
key="BATTERY_PERCENTAGE",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
@@ -149,7 +148,7 @@ ECOWITT_SENSORS_MAPPING: Final = {
EcoWittSensorTypes.CO2_PPM: SensorEntityDescription(
key="CO2_PPM",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.LUX: SensorEntityDescription(
@@ -263,13 +262,13 @@ ECOWITT_SENSORS_MAPPING: Final = {
),
EcoWittSensorTypes.PERCENTAGE: SensorEntityDescription(
key="PERCENTAGE",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.SOIL_MOISTURE: SensorEntityDescription(
key="SOIL_MOISTURE",
device_class=SensorDeviceClass.MOISTURE,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.DISTANCE_MM: SensorEntityDescription(
@@ -286,13 +285,13 @@ ECOWITT_SENSORS_MAPPING: Final = {
EcoWittSensorTypes.PM1: SensorEntityDescription(
key="PM1",
device_class=SensorDeviceClass.PM1,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.PM4: SensorEntityDescription(
key="PM4",
device_class=SensorDeviceClass.PM4,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
}
@@ -65,6 +65,14 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
"/ivp/meters/readings",
"/ivp/pdm/device_data",
"/home",
"/inventory.json?deleted=1",
"/admin/lib/acb_config",
"/ivp/sc/sched",
"/admin/lib/network_display",
"/admin/lib/wireless_display",
"/ivp/ensemble/relay",
"/ivp/livedata/status",
"/ivp/pdm/energy",
]
for end_point in end_points:
@@ -134,16 +142,15 @@ async def async_get_config_entry_diagnostics(
"encharge_power": envoy_data.encharge_power,
"encharge_aggregate": envoy_data.encharge_aggregate,
"enpower": envoy_data.enpower,
"acb_power": envoy_data.acb_power,
"acb_inventory": envoy_data.acb_inventory,
"battery_aggregate": envoy_data.battery_aggregate,
"collar": envoy_data.collar,
"c6cc": envoy_data.c6cc,
"system_consumption": envoy_data.system_consumption,
"system_production": envoy_data.system_production,
"system_consumption_phases": envoy_data.system_consumption_phases,
"system_production_phases": envoy_data.system_production_phases,
"ctmeter_production": envoy_data.ctmeter_production,
"ctmeter_consumption": envoy_data.ctmeter_consumption,
"ctmeter_storage": envoy_data.ctmeter_storage,
"ctmeter_production_phases": envoy_data.ctmeter_production_phases,
"ctmeter_consumption_phases": envoy_data.ctmeter_consumption_phases,
"ctmeter_storage_phases": envoy_data.ctmeter_storage_phases,
"ctmeters": envoy_data.ctmeters,
"ctmeters_phases": envoy_data.ctmeters_phases,
"dry_contact_status": envoy_data.dry_contact_status,
+6 -2
View File
@@ -12,7 +12,6 @@ from aioesphomeapi import (
EntityCategory as EsphomeEntityCategory,
EntityInfo,
EntityState,
build_device_unique_id,
)
import voluptuous as vol
@@ -32,7 +31,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
# Import config flow so that it's added to the registry
from .entry_data import DeviceEntityKey, ESPHomeConfigEntry, RuntimeEntryData
from .entry_data import (
DeviceEntityKey,
ESPHomeConfigEntry,
RuntimeEntryData,
build_device_unique_id,
)
from .enum_mapper import EsphomeEnumMapper
_LOGGER = logging.getLogger(__name__)
+17 -21
View File
@@ -45,7 +45,7 @@ from aioesphomeapi import (
UserService,
ValveInfo,
WaterHeaterInfo,
build_device_unique_id,
build_unique_id,
)
from aioesphomeapi.model import ButtonInfo
from bleak_esphome.backend.device import ESPHomeBluetoothDevice
@@ -103,6 +103,22 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = {
}
def build_device_unique_id(mac: str, entity_info: EntityInfo) -> str:
"""Build unique ID for entity, appending @device_id if it belongs to a sub-device.
This wrapper around build_unique_id ensures that entities belonging to sub-devices
have their device_id appended to the unique_id to handle proper migration when
entities move between devices.
"""
base_unique_id = build_unique_id(mac, entity_info)
# If entity belongs to a sub-device, append @device_id
if entity_info.device_id:
return f"{base_unique_id}@{entity_info.device_id}"
return base_unique_id
class StoreData(TypedDict, total=False):
"""ESPHome storage data."""
@@ -294,31 +310,11 @@ class RuntimeEntryData:
infos_by_type: defaultdict[type[EntityInfo], list[EntityInfo]] = defaultdict(
list
)
ent_reg = er.async_get(hass)
registry_get_entity = ent_reg.async_get_entity_id
for info in infos:
info_type = type(info)
if platform := info_types_to_platform.get(info_type):
needed_platforms.add(platform)
infos_by_type[info_type].append(info)
# Migrate legacy unique ids to the version 3 format that fixes
# UTF-8 collisions. Skip when a version 3 id already exists so a
# downgrade then upgrade keeps the original entity. When two
# legacy ids collided (the bug this fixes) only one registry
# entry exists for it, so the first iterated info claims it and
# the rest get fresh version 3 ids.
old_unique_id = build_device_unique_id(mac, info, version=1)
new_unique_id = build_device_unique_id(mac, info, version=3)
if (
old_unique_id != new_unique_id
and (
old_entry := registry_get_entity(
platform, DOMAIN, old_unique_id
)
)
and not registry_get_entity(platform, DOMAIN, new_unique_id)
):
ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id)
else:
_LOGGER.warning(
"Entity type %s is not supported in this version of Home Assistant",
+7 -8
View File
@@ -1417,14 +1417,13 @@ async def async_replace_device(
upper_mac = new_mac.upper()
old_upper_mac = old_mac.upper()
for entity in er.async_entries_for_config_entry(ent_reg, entry.entry_id):
# The mac is the leading segment of the unique id in every format,
# so swap the prefix without parsing the rest.
if entity.unique_id.startswith(old_upper_mac):
new_unique_id = upper_mac + entity.unique_id[len(old_upper_mac) :]
if new_unique_id != entity.unique_id:
ent_reg.async_update_entity(
entity.entity_id, new_unique_id=new_unique_id
)
# <upper_mac>-<entity type>-<object_id>
old_unique_id = entity.unique_id.split("-")
new_unique_id = "-".join([upper_mac, *old_unique_id[1:]])
if entity.unique_id != new_unique_id and entity.unique_id.startswith(
old_upper_mac
):
ent_reg.async_update_entity(entity.entity_id, new_unique_id=new_unique_id)
domain_data = DomainData.get(hass)
store = domain_data.get_or_create_store(hass, entry)
+10 -10
View File
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
from homeassistant.const import UnitOfDensity
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -63,7 +63,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
key=ATTR_C6H6,
value=lambda sensors: sensors.c6h6.value if sensors.c6h6 else None,
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
translation_key="c6h6",
),
@@ -72,7 +72,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.co.value if sensors.co else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.CO,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -80,7 +80,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.no.value if sensors.no else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.NITROGEN_MONOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -88,7 +88,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.no2.value if sensors.no2 else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -104,7 +104,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
translation_key=ATTR_NOX,
value=lambda sensors: sensors.nox.value if sensors.nox else None,
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -112,7 +112,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.o3.value if sensors.o3 else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.OZONE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -128,7 +128,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.pm10.value if sensors.pm10 else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -144,7 +144,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.pm25.value if sensors.pm25 else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
@@ -160,7 +160,7 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = (
value=lambda sensors: sensors.so2.value if sensors.so2 else None,
suggested_display_precision=0,
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
state_class=SensorStateClass.MEASUREMENT,
),
GiosSensorEntityDescription(
+1 -1
View File
@@ -92,7 +92,7 @@ class SupervisorJobs:
# We catch all errors to prevent an error in one from stopping the others
for match in [job for job in self._jobs.values() if subscription.matches(job)]:
try:
return subscription.event_callback(match)
subscription.event_callback(match)
except Exception as err: # noqa: BLE001
_LOGGER.error(
"Error encountered processing Supervisor Job (%s %s %s) - %s",
@@ -161,5 +161,7 @@ SENSOR_KEYS: Final[dict[int, list[str]]] = {
IndevoltSolar.CUMULATIVE_PRODUCTION,
IndevoltBattery.GEN_2_CYCLE_COUNT,
IndevoltBattery.GEN_2_TRANSFORMER_TEMPERATURE,
IndevoltBattery.REMAINING_CHARGING_TIME,
IndevoltBattery.REMAINING_DISCHARGING_TIME,
],
}
@@ -27,6 +27,7 @@ from homeassistant.const import (
UnitOfFrequency,
UnitOfPower,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -45,6 +46,7 @@ class IndevoltSensorEntityDescription(SensorEntityDescription):
state_mapping: dict[str | int, str] = field(default_factory=dict)
generation: tuple[int, ...] = (1, 2)
energy_mode: IndevoltEnergyMode | None = None
charge_discharge_state: int | None = None
SENSORS: Final = (
@@ -242,6 +244,26 @@ SENSORS: Final = (
state_mapping={1000: "static", 1001: "charging", 1002: "discharging"},
device_class=SensorDeviceClass.ENUM,
),
IndevoltSensorEntityDescription(
key=IndevoltBattery.REMAINING_CHARGING_TIME,
generation=(2,),
translation_key="remaining_charging_time",
native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
charge_discharge_state=1001,
entity_registry_enabled_default=False,
),
IndevoltSensorEntityDescription(
key=IndevoltBattery.REMAINING_DISCHARGING_TIME,
generation=(2,),
translation_key="remaining_discharging_time",
native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
charge_discharge_state=1002,
entity_registry_enabled_default=False,
),
IndevoltSensorEntityDescription(
key=IndevoltBattery.SOC,
translation_key="battery_soc",
@@ -948,6 +970,14 @@ class IndevoltSensorEntity(IndevoltEntity, SensorEntity):
if energy_mode != self.entity_description.energy_mode:
return False
# Check whether the battery is not in the required charge/discharge state
if (
self.entity_description.charge_discharge_state is not None
and self.coordinator.data.get(IndevoltBattery.CHARGE_DISCHARGE_STATE)
!= self.entity_description.charge_discharge_state
):
return False
# Check whether inverter is reporting 0 degrees with heater not active (thus reporting to indicate "idle")
# Pending fix by Indevolt: https://discord.com/channels/1417471269942591571/1510277757689659522
if self.entity_description.key == IndevoltBattery.GEN_1_INVERTER_TEMPERATURE:
@@ -365,6 +365,12 @@
"realtime_target_soc": {
"name": "Real-time target SOC"
},
"remaining_charging_time": {
"name": "Remaining charging time"
},
"remaining_discharging_time": {
"name": "Remaining discharging time"
},
"serial_number": {
"name": "Serial number"
},
+16 -17
View File
@@ -16,13 +16,12 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfDensity,
UnitOfPressure,
UnitOfRatio,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
@@ -97,7 +96,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_BME280_HUMIDITY,
translation_key="bme280_humidity",
suggested_display_precision=1,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.bme280_humidity,
@@ -169,7 +168,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_HECA_HUMIDITY,
translation_key="heca_humidity",
suggested_display_precision=1,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.heca_humidity,
@@ -187,7 +186,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_MHZ14A_CARBON_DIOXIDE,
translation_key="mhz14a_carbon_dioxide",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.mhz14a_carbon_dioxide,
@@ -208,7 +207,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_PMSX003_P0,
translation_key="pmsx003_pm1",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM1,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.pms_p0,
@@ -217,7 +216,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_PMSX003_P1,
translation_key="pmsx003_pm10",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.pms_p1,
@@ -226,7 +225,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_PMSX003_P2,
translation_key="pmsx003_pm25",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.pms_p2,
@@ -247,7 +246,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SDS011_P1,
translation_key="sds011_pm10",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sds011_p1,
@@ -256,7 +255,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SDS011_P2,
translation_key="sds011_pm25",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sds011_p2,
@@ -265,7 +264,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SHT3X_HUMIDITY,
translation_key="sht3x_humidity",
suggested_display_precision=1,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sht3x_humidity,
@@ -295,7 +294,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SPS30_P0,
translation_key="sps30_pm1",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM1,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sps30_p0,
@@ -304,7 +303,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SPS30_P1,
translation_key="sps30_pm10",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sps30_p1,
@@ -313,7 +312,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SPS30_P2,
translation_key="sps30_pm25",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sps30_p2,
@@ -322,7 +321,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_SPS30_P4,
translation_key="sps30_pm4",
suggested_display_precision=0,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM4,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.sps30_p4,
@@ -331,7 +330,7 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
key=ATTR_DHT22_HUMIDITY,
translation_key="dht22_humidity",
suggested_display_precision=1,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
value=lambda sensors: sensors.dht22_humidity,
+4 -10
View File
@@ -12,13 +12,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
StateType,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
UnitOfPressure,
UnitOfTemperature,
)
from homeassistant.const import UnitOfPressure, UnitOfRatio, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -54,13 +48,13 @@ ENTITIES: tuple[PranaSensorEntityDescription, ...] = (
PranaSensorEntityDescription(
key=PranaSensorType.HUMIDITY,
value_fn=lambda coord: coord.data.humidity,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
),
PranaSensorEntityDescription(
key=PranaSensorType.VOC,
value_fn=lambda coord: coord.data.voc,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_BILLION,
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
),
PranaSensorEntityDescription(
@@ -72,7 +66,7 @@ ENTITIES: tuple[PranaSensorEntityDescription, ...] = (
PranaSensorEntityDescription(
key=PranaSensorType.CO2,
value_fn=lambda coord: coord.data.co2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
device_class=SensorDeviceClass.CO2,
),
PranaSensorEntityDescription(
+6 -2
View File
@@ -286,7 +286,10 @@ async def register_callbacks(
return async_camera_wake
host.api.baichuan.register_callback(
"privacy_mode_change", async_privacy_mode_change, 623
"privacy_mode_change_623", async_privacy_mode_change, 623
)
host.api.baichuan.register_callback(
"privacy_mode_change_574", async_privacy_mode_change, 574
)
for channel in host.api.channels:
if host.api.supported(channel, "battery"):
@@ -306,7 +309,8 @@ async def async_unload_entry(
await host.stop()
host.api.baichuan.unregister_callback("privacy_mode_change")
host.api.baichuan.unregister_callback("privacy_mode_change_623")
host.api.baichuan.unregister_callback("privacy_mode_change_574")
for channel in host.api.channels:
if host.api.supported(channel, "battery"):
host.api.baichuan.unregister_callback(f"camera_{channel}_wake")
@@ -75,6 +75,7 @@ LIGHT_ENTITIES = (
ReolinkLightEntityDescription(
key="status_led",
cmd_key="GetPowerLed",
cmd_id=208,
translation_key="status_led",
entity_category=EntityCategory.CONFIG,
supported=lambda api, ch: api.supported(ch, "power_led"),
@@ -20,5 +20,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.21.2"]
"requirements": ["reolink-aio==0.21.3"]
}
@@ -195,6 +195,7 @@ NUMBER_ENTITIES = (
key="volume",
cmd_key="GetAudioCfg",
translation_key="volume",
cmd_id=264,
entity_category=EntityCategory.CONFIG,
native_step=1,
native_min_value=0,
@@ -206,6 +207,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="volume_speak",
cmd_key="GetAudioCfg",
cmd_id=264,
translation_key="volume_speak",
entity_category=EntityCategory.CONFIG,
native_step=1,
@@ -218,6 +220,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="volume_doorbell",
cmd_key="GetAudioCfg",
cmd_id=264,
translation_key="volume_doorbell",
entity_category=EntityCategory.CONFIG,
native_step=1,
@@ -269,6 +272,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="pir_sensitivity",
cmd_key="GetPirInfo",
cmd_id=212,
translation_key="pir_sensitivity",
entity_category=EntityCategory.CONFIG,
native_step=1,
@@ -281,6 +285,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="pir_interval",
cmd_key="GetPirInfo",
cmd_id=212,
translation_key="pir_interval",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
@@ -296,6 +301,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_face_sensititvity",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_face_sensitivity",
entity_category=EntityCategory.CONFIG,
native_step=1,
@@ -310,6 +316,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_person_sensititvity",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_person_sensitivity",
entity_category=EntityCategory.CONFIG,
native_step=1,
@@ -324,6 +331,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_vehicle_sensititvity",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_vehicle_sensitivity",
entity_category=EntityCategory.CONFIG,
native_step=1,
@@ -338,6 +346,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_non_motor_vehicle_sensitivity",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_non_motor_vehicle_sensitivity",
entity_category=EntityCategory.CONFIG,
native_step=1,
@@ -355,6 +364,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_package_sensititvity",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_package_sensitivity",
entity_category=EntityCategory.CONFIG,
native_step=1,
@@ -369,6 +379,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_pet_sensititvity",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_pet_sensitivity",
entity_category=EntityCategory.CONFIG,
native_step=1,
@@ -385,6 +396,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_pet_sensititvity",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_animal_sensitivity",
entity_category=EntityCategory.CONFIG,
native_step=1,
@@ -411,6 +423,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_face_delay",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_face_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
@@ -428,6 +441,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_person_delay",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_person_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
@@ -445,6 +459,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_non_motor_vehicle_delay",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_non_motor_vehicle_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
@@ -464,6 +479,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_vehicle_delay",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_vehicle_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
@@ -481,6 +497,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_package_delay",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_package_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
@@ -498,6 +515,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_pet_delay",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_pet_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
@@ -517,6 +535,7 @@ NUMBER_ENTITIES = (
ReolinkNumberEntityDescription(
key="ai_pet_delay",
cmd_key="GetAiAlarm",
cmd_id=342,
translation_key="ai_animal_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
@@ -185,6 +185,7 @@ SELECT_ENTITIES = (
ReolinkSelectEntityDescription(
key="status_led",
cmd_key="GetPowerLed",
cmd_id=208,
translation_key="doorbell_led",
entity_category=EntityCategory.CONFIG,
get_options=lambda api, ch: api.doorbell_led_list(ch),
@@ -232,6 +233,7 @@ SELECT_ENTITIES = (
ReolinkSelectEntityDescription(
key="main_frame_rate",
cmd_key="GetEnc",
cmd_id=56,
translation_key="main_frame_rate",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@@ -244,6 +246,7 @@ SELECT_ENTITIES = (
ReolinkSelectEntityDescription(
key="sub_frame_rate",
cmd_key="GetEnc",
cmd_id=56,
translation_key="sub_frame_rate",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@@ -256,6 +259,7 @@ SELECT_ENTITIES = (
ReolinkSelectEntityDescription(
key="main_bit_rate",
cmd_key="GetEnc",
cmd_id=56,
translation_key="main_bit_rate",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@@ -268,6 +272,7 @@ SELECT_ENTITIES = (
ReolinkSelectEntityDescription(
key="sub_bit_rate",
cmd_key="GetEnc",
cmd_id=56,
translation_key="sub_bit_rate",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@@ -280,6 +285,7 @@ SELECT_ENTITIES = (
ReolinkSelectEntityDescription(
key="main_encoding",
cmd_key="GetEnc",
cmd_id=56,
translation_key="main_encoding",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@@ -291,6 +297,7 @@ SELECT_ENTITIES = (
ReolinkSelectEntityDescription(
key="sub_encoding",
cmd_key="GetEnc",
cmd_id=56,
translation_key="sub_encoding",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@@ -316,6 +323,7 @@ SELECT_ENTITIES = (
ReolinkSelectEntityDescription(
key="post_rec_time",
cmd_key="GetRec",
cmd_id=54,
translation_key="post_rec_time",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@@ -340,6 +348,7 @@ HOST_SELECT_ENTITIES = (
ReolinkHostSelectEntityDescription(
key="packing_time",
cmd_key="GetRec",
cmd_id=54,
translation_key="packing_time",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@@ -74,6 +74,7 @@ SWITCH_ENTITIES = (
ReolinkSwitchEntityDescription(
key="ir_lights",
cmd_key="GetIrLights",
cmd_id=208,
translation_key="ir_lights",
entity_category=EntityCategory.CONFIG,
supported=lambda api, ch: api.supported(ch, "ir_lights"),
@@ -83,6 +84,7 @@ SWITCH_ENTITIES = (
ReolinkSwitchEntityDescription(
key="record_audio",
cmd_key="GetEnc",
cmd_id=56,
translation_key="record_audio",
entity_category=EntityCategory.CONFIG,
supported=lambda api, ch: api.supported(ch, "audio"),
@@ -92,6 +94,7 @@ SWITCH_ENTITIES = (
ReolinkSwitchEntityDescription(
key="siren_on_event",
cmd_key="GetAudioAlarm",
cmd_id=232,
translation_key="siren_on_event",
entity_category=EntityCategory.CONFIG,
supported=lambda api, ch: api.supported(ch, "siren"),
@@ -136,6 +139,7 @@ SWITCH_ENTITIES = (
ReolinkSwitchEntityDescription(
key="email",
cmd_key="GetEmail",
cmd_id=217,
translation_key="email",
entity_category=EntityCategory.CONFIG,
supported=lambda api, ch: api.supported(ch, "email") and api.is_nvr,
@@ -145,6 +149,7 @@ SWITCH_ENTITIES = (
ReolinkSwitchEntityDescription(
key="ftp_upload",
cmd_key="GetFtp",
cmd_id=70,
translation_key="ftp_upload",
entity_category=EntityCategory.CONFIG,
supported=lambda api, ch: api.supported(ch, "ftp") and api.is_nvr,
@@ -163,6 +168,7 @@ SWITCH_ENTITIES = (
ReolinkSwitchEntityDescription(
key="record",
cmd_key="GetRec",
cmd_id=81,
translation_key="record",
entity_category=EntityCategory.CONFIG,
supported=lambda api, ch: api.supported(ch, "rec_enable") and api.is_nvr,
@@ -200,6 +206,7 @@ SWITCH_ENTITIES = (
ReolinkSwitchEntityDescription(
key="doorbell_button_sound",
cmd_key="GetAudioCfg",
cmd_id=264,
translation_key="doorbell_button_sound",
entity_category=EntityCategory.CONFIG,
supported=lambda api, ch: api.supported(ch, "doorbell_button_sound"),
@@ -209,6 +216,7 @@ SWITCH_ENTITIES = (
ReolinkSwitchEntityDescription(
key="pir_enabled",
cmd_key="GetPirInfo",
cmd_id=212,
translation_key="pir_enabled",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@@ -219,6 +227,7 @@ SWITCH_ENTITIES = (
ReolinkSwitchEntityDescription(
key="pir_reduce_alarm",
cmd_key="GetPirInfo",
cmd_id=212,
translation_key="pir_reduce_alarm",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
@@ -260,6 +269,7 @@ HOST_SWITCH_ENTITIES = (
ReolinkHostSwitchEntityDescription(
key="email",
cmd_key="GetEmail",
cmd_id=217,
translation_key="email",
entity_category=EntityCategory.CONFIG,
supported=lambda api: api.supported(None, "email") and not api.is_hub,
@@ -269,6 +279,7 @@ HOST_SWITCH_ENTITIES = (
ReolinkHostSwitchEntityDescription(
key="ftp_upload",
cmd_key="GetFtp",
cmd_id=70,
translation_key="ftp_upload",
entity_category=EntityCategory.CONFIG,
supported=lambda api: api.supported(None, "ftp") and not api.is_hub,
@@ -287,6 +298,7 @@ HOST_SWITCH_ENTITIES = (
ReolinkHostSwitchEntityDescription(
key="record",
cmd_key="GetRec",
cmd_id=81,
translation_key="record",
entity_category=EntityCategory.CONFIG,
supported=lambda api: api.supported(None, "rec_enable") and not api.is_hub,
+3 -4
View File
@@ -15,14 +15,12 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
CONF_DEVICES,
CONF_NAME,
CONF_SENSOR_TYPE,
CONF_UNIT_OF_MEASUREMENT,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
UV_INDEX,
UnitOfElectricCurrent,
UnitOfElectricPotential,
@@ -30,6 +28,7 @@ from homeassistant.const import (
UnitOfPower,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfRatio,
UnitOfSpeed,
UnitOfTemperature,
UnitOfVolumetricFlux,
@@ -85,7 +84,7 @@ SENSOR_TYPES = (
name="CO2 air quality",
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
),
SensorEntityDescription(
key="command",
@@ -140,7 +139,7 @@ SENSOR_TYPES = (
name="Humidity",
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
),
SensorEntityDescription(
key="humidity_status",
@@ -20,7 +20,7 @@
"loggers": ["roborock"],
"quality_scale": "silver",
"requirements": [
"python-roborock==5.14.2",
"python-roborock==5.21.0",
"vacuum-map-parser-roborock==0.1.5"
]
}
+4 -2
View File
@@ -35,7 +35,7 @@ class RoborockNumberDescription(NumberEntityDescription):
trait: Callable[[PropertiesApi], Any | None]
"""Function to determine if number entity is supported by the device."""
get_value: Callable[[Any], float]
get_value: Callable[[Any], float | None]
"""Function to get the value from the trait."""
set_value: Callable[[Any, float], Coroutine[Any, Any, None]]
@@ -51,7 +51,9 @@ NUMBER_DESCRIPTIONS: list[RoborockNumberDescription] = [
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.CONFIG,
trait=lambda api: api.sound_volume,
get_value=lambda trait: float(trait.volume),
get_value=lambda trait: (
float(trait.volume) if trait.volume is not None else None
),
set_value=lambda trait, value: trait.set_volume(int(value)),
)
]
+12 -13
View File
@@ -30,6 +30,13 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
def _build_time(hour: int | None, minute: int | None) -> datetime.time | None:
"""Build a time, returning None when either component is missing."""
if hour is None or minute is None:
return None
return datetime.time(hour=hour, minute=minute)
@dataclass(frozen=True, kw_only=True)
class RoborockTimeDescription(TimeEntityDescription):
"""Class to describe a Roborock time entity."""
@@ -37,7 +44,7 @@ class RoborockTimeDescription(TimeEntityDescription):
trait: Callable[[Any], Any | None]
"""Function to determine if time entity is supported by the device."""
get_value: Callable[[Any], datetime.time]
get_value: Callable[[Any], datetime.time | None]
"""Function to get the value from the trait."""
update_value: Callable[[Any, datetime.time], Coroutine[Any, Any, None]]
@@ -58,9 +65,7 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
end_minute=trait.end_minute,
)
),
get_value=lambda trait: datetime.time(
hour=trait.start_hour, minute=trait.start_minute
),
get_value=lambda trait: _build_time(trait.start_hour, trait.start_minute),
entity_category=EntityCategory.CONFIG,
),
RoborockTimeDescription(
@@ -76,9 +81,7 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
end_minute=desired_time.minute,
)
),
get_value=lambda trait: datetime.time(
hour=trait.end_hour, minute=trait.end_minute
),
get_value=lambda trait: _build_time(trait.end_hour, trait.end_minute),
entity_category=EntityCategory.CONFIG,
),
RoborockTimeDescription(
@@ -94,9 +97,7 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
end_minute=trait.end_minute,
)
),
get_value=lambda trait: datetime.time(
hour=trait.start_hour, minute=trait.start_minute
),
get_value=lambda trait: _build_time(trait.start_hour, trait.start_minute),
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
),
@@ -113,9 +114,7 @@ TIME_DESCRIPTIONS: list[RoborockTimeDescription] = [
end_minute=desired_time.minute,
)
),
get_value=lambda trait: datetime.time(
hour=trait.end_hour, minute=trait.end_minute
),
get_value=lambda trait: _build_time(trait.end_hour, trait.end_minute),
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
),
+5 -5
View File
@@ -17,7 +17,7 @@ from homeassistant.components.number import (
NumberMode,
RestoreNumber,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
from homeassistant.const import EntityCategory, UnitOfRatio, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -183,7 +183,7 @@ BLOCK_NUMBERS: dict[tuple[str, str], BlockNumberDescription] = {
("device", "valvePos"): BlockNumberDescription(
key="device|valvepos",
translation_key="valve_position",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
available=lambda block: cast(int, block.valveError) != 1,
entity_category=EntityCategory.CONFIG,
native_min_value=0,
@@ -291,7 +291,7 @@ RPC_NUMBERS: Final = {
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
method="blu_trv_set_valve_position",
removal_condition=lambda config, _, key: (
config[key].get("enable", True) is True
@@ -307,7 +307,7 @@ RPC_NUMBERS: Final = {
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
method="cury_set",
slot="left",
available=lambda status: (
@@ -325,7 +325,7 @@ RPC_NUMBERS: Final = {
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
method="cury_set",
slot="right",
available=lambda status: (
+13 -14
View File
@@ -17,10 +17,8 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfApparentPower,
@@ -30,6 +28,7 @@ from homeassistant.const import (
UnitOfFrequency,
UnitOfPower,
UnitOfPressure,
UnitOfRatio,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
@@ -203,7 +202,7 @@ class RpcBluTrvSensor(RpcSensor):
BLOCK_SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
("device", "battery"): BlockSensorDescription(
key="device|battery",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
removal_condition=lambda settings, _: settings.get("external_power") == 1,
@@ -350,7 +349,7 @@ BLOCK_SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
("sensor", "concentration"): BlockSensorDescription(
key="sensor|concentration",
translation_key="gas_concentration",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
),
("sensor", "temp"): BlockSensorDescription(
@@ -373,7 +372,7 @@ BLOCK_SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
),
("sensor", "humidity"): BlockSensorDescription(
key="sensor|humidity",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
suggested_display_precision=1,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
@@ -398,7 +397,7 @@ BLOCK_SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
("relay", "totalWorkTime"): BlockSensorDescription(
key="relay|totalWorkTime",
translation_key="lamp_life",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
value=get_shelly_air_lamp_life,
suggested_display_precision=1,
entity_category=EntityCategory.DIAGNOSTIC,
@@ -1255,7 +1254,7 @@ RPC_SENSORS: Final = {
"humidity_rh": RpcSensorDescription(
key="humidity",
sub_key="rh",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
suggested_display_precision=1,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
@@ -1266,7 +1265,7 @@ RPC_SENSORS: Final = {
"battery": RpcSensorDescription(
key="devicepower",
sub_key="battery",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
value=lambda status, _: status["percent"],
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
@@ -1296,7 +1295,7 @@ RPC_SENSORS: Final = {
key="input",
sub_key="percent",
translation_key="analog",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
removal_condition=lambda config, _, key: (
config[key]["type"] != "analog" or config[key]["enable"] is False
@@ -1389,7 +1388,7 @@ RPC_SENSORS: Final = {
key="blutrv",
sub_key="pos",
translation_key="valve_position",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
removal_condition=lambda config, _, key: (
@@ -1400,7 +1399,7 @@ RPC_SENSORS: Final = {
"blutrv_battery": RpcSensorDescription(
key="blutrv",
sub_key="battery",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
@@ -1448,7 +1447,7 @@ RPC_SENSORS: Final = {
"number_current_humidity": RpcSensorDescription(
key="number",
sub_key="value",
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
suggested_display_precision=1,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
@@ -1686,7 +1685,7 @@ RPC_SENSORS: Final = {
translation_key="left_slot_level",
value=lambda status, _: status["left"]["vial"]["level"],
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
available=lambda status: (
(left := status["left"]) is not None
@@ -1710,7 +1709,7 @@ RPC_SENSORS: Final = {
translation_key="right_slot_level",
value=lambda status, _: status["right"]["vial"]["level"],
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
available=lambda status: (
(right := status["right"]) is not None
@@ -1 +0,0 @@
"""The thermoworks_smoke component."""
@@ -1,11 +0,0 @@
{
"domain": "thermoworks_smoke",
"name": "ThermoWorks Smoke",
"codeowners": [],
"disabled": "This integration is disabled because it creates an unresolvable dependency conflict.",
"documentation": "https://www.home-assistant.io/integrations/thermoworks_smoke",
"iot_class": "cloud_polling",
"loggers": ["thermoworks_smoke"],
"quality_scale": "legacy",
"requirements": ["stringcase==1.2.0", "thermoworks-smoke==0.1.8"]
}
@@ -1,166 +0,0 @@
"""Support for getting the state of a Thermoworks Smoke Thermometer.
Requires Smoke Gateway Wifi with an internet connection.
"""
import logging
from requests import RequestException
from requests.exceptions import HTTPError
from stringcase import camelcase
import thermoworks_smoke
import voluptuous as vol
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
)
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
CONF_EMAIL,
CONF_EXCLUDE,
CONF_MONITORED_CONDITIONS,
CONF_PASSWORD,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import snakecase
_LOGGER = logging.getLogger(__name__)
PROBE_1 = "probe1"
PROBE_2 = "probe2"
PROBE_1_MIN = "probe1_min"
PROBE_1_MAX = "probe1_max"
PROBE_2_MIN = "probe2_min"
PROBE_2_MAX = "probe2_max"
BATTERY_LEVEL = "battery"
FIRMWARE = "firmware"
SERIAL_REGEX = "^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$"
# map types to labels
SENSOR_TYPES = {
PROBE_1: "Probe 1",
PROBE_2: "Probe 2",
PROBE_1_MIN: "Probe 1 Min",
PROBE_1_MAX: "Probe 1 Max",
PROBE_2_MIN: "Probe 2 Min",
PROBE_2_MAX: "Probe 2 Max",
}
# exclude these keys from thermoworks data
EXCLUDE_KEYS = [FIRMWARE]
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_EMAIL): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_MONITORED_CONDITIONS, default=[PROBE_1, PROBE_2]): vol.All(
cv.ensure_list, [vol.In(SENSOR_TYPES)]
),
vol.Optional(CONF_EXCLUDE, default=[]): vol.All(
cv.ensure_list, [cv.matches_regex(SERIAL_REGEX)]
),
}
)
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the thermoworks sensor."""
email = config[CONF_EMAIL]
password = config[CONF_PASSWORD]
monitored_variables = config[CONF_MONITORED_CONDITIONS]
excluded = config[CONF_EXCLUDE]
try:
mgr = thermoworks_smoke.initialize_app(email, password, True, excluded)
except HTTPError as error:
msg = f"{error.strerror}"
if "EMAIL_NOT_FOUND" in msg or "INVALID_PASSWORD" in msg:
_LOGGER.error("Invalid email and password combination")
else:
_LOGGER.error(msg)
else:
add_entities(
(
ThermoworksSmokeSensor(variable, serial, mgr)
for serial in mgr.serials()
for variable in monitored_variables
),
True,
)
class ThermoworksSmokeSensor(SensorEntity):
"""Implementation of a thermoworks smoke sensor."""
def __init__(self, sensor_type, serial, mgr):
"""Initialize the sensor."""
self.type = sensor_type
self.serial = serial
self.mgr = mgr
self._attr_name = f"{mgr.name(serial)} {SENSOR_TYPES[sensor_type]}"
self._attr_native_unit_of_measurement = UnitOfTemperature.FAHRENHEIT
self._attr_unique_id = f"{serial}-{sensor_type}"
self._attr_device_class = SensorDeviceClass.TEMPERATURE
self.update_unit()
def update_unit(self):
"""Set the units from the data."""
if PROBE_2 in self.type:
self._attr_native_unit_of_measurement = self.mgr.units(self.serial, PROBE_2)
else:
self._attr_native_unit_of_measurement = self.mgr.units(self.serial, PROBE_1)
def update(self) -> None:
"""Get the monitored data from firebase."""
try:
values = self.mgr.data(self.serial)
# set state from data based on type of sensor
self._attr_native_value = values.get(camelcase(self.type))
# set units
self.update_unit()
# set basic attributes for all sensors
self._attr_extra_state_attributes = {
"time": values["time"],
"localtime": values["localtime"],
}
# set extended attributes for main probe sensors
if self.type in (PROBE_1, PROBE_2):
for key, val in values.items():
# add all attributes that don't contain any probe name
# or contain a matching probe name
if (self.type == PROBE_1 and key.find(PROBE_2) == -1) or (
self.type == PROBE_2 and key.find(PROBE_1) == -1
):
if key == BATTERY_LEVEL:
key = ATTR_BATTERY_LEVEL
else:
# strip probe label and convert to snake_case
key = snakecase(key.replace(self.type, ""))
# add to attrs
if key and key not in EXCLUDE_KEYS:
self._attr_extra_state_attributes[key] = val
# store actual unit because attributes are not converted
self._attr_extra_state_attributes["unit_of_min_max"] = (
self._attr_native_unit_of_measurement
)
except RequestException, ValueError, KeyError:
_LOGGER.warning("Could not update status for %s", self.name)
+9 -10
View File
@@ -21,13 +21,12 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
CONF_API_KEY,
PERCENTAGE,
UnitOfDensity,
UnitOfIrradiance,
UnitOfLength,
UnitOfPressure,
UnitOfRatio,
UnitOfSpeed,
UnitOfTemperature,
)
@@ -174,7 +173,7 @@ SENSOR_TYPES = (
key="cloud_cover",
translation_key="cloud_cover",
attribute=TMRW_ATTR_CLOUD_COVER,
native_unit_of_measurement=PERCENTAGE,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
),
# Data comes in as m/s, convert to mi/h for imperial
TomorrowioSensorEntityDescription(
@@ -200,7 +199,7 @@ SENSOR_TYPES = (
TomorrowioSensorEntityDescription(
key="ozone",
attribute=TMRW_ATTR_OZONE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
multiplication_factor=convert_ppb_to_ugm3(48),
device_class=SensorDeviceClass.OZONE,
state_class=SensorStateClass.MEASUREMENT,
@@ -208,14 +207,14 @@ SENSOR_TYPES = (
TomorrowioSensorEntityDescription(
key="particulate_matter_2_5_mm",
attribute=TMRW_ATTR_PARTICULATE_MATTER_25,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM25,
state_class=SensorStateClass.MEASUREMENT,
),
TomorrowioSensorEntityDescription(
key="particulate_matter_10_mm",
attribute=TMRW_ATTR_PARTICULATE_MATTER_10,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.PM10,
state_class=SensorStateClass.MEASUREMENT,
),
@@ -224,7 +223,7 @@ SENSOR_TYPES = (
TomorrowioSensorEntityDescription(
key="nitrogen_dioxide",
attribute=TMRW_ATTR_NITROGEN_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
multiplication_factor=convert_ppb_to_ugm3(46.01),
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
state_class=SensorStateClass.MEASUREMENT,
@@ -233,7 +232,7 @@ SENSOR_TYPES = (
TomorrowioSensorEntityDescription(
key="carbon_monoxide",
attribute=TMRW_ATTR_CARBON_MONOXIDE,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=UnitOfRatio.PARTS_PER_MILLION,
multiplication_factor=1 / 1000,
device_class=SensorDeviceClass.CO,
state_class=SensorStateClass.MEASUREMENT,
@@ -243,7 +242,7 @@ SENSOR_TYPES = (
TomorrowioSensorEntityDescription(
key="sulphur_dioxide",
attribute=TMRW_ATTR_SULPHUR_DIOXIDE,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
multiplication_factor=convert_ppb_to_ugm3(64.07),
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
state_class=SensorStateClass.MEASUREMENT,
@@ -145,9 +145,7 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
translation_placeholders={"error": repr(err)},
) from err
except (
exceptions.CannotConnect,
exceptions.AlreadyLogged,
exceptions.GenericLoginError,
exceptions.VodafoneError,
JSONDecodeError,
) as err:
if isinstance(err, JSONDecodeError):
@@ -7201,12 +7201,6 @@
"config_flow": true,
"iot_class": "local_push"
},
"thermoworks_smoke": {
"name": "ThermoWorks Smoke",
"integration_type": "hub",
"config_flow": false,
"iot_class": "cloud_polling"
},
"thethingsnetwork": {
"name": "The Things Network",
"integration_type": "hub",
+1 -1
View File
@@ -71,7 +71,7 @@ typing-extensions>=4.15.0,<5.0
ulid-transform==2.2.9
urllib3>=2.0
uv==0.11.21
voluptuous-openapi==0.3.0
voluptuous-openapi==0.4.1
voluptuous-serialize==2.7.0
voluptuous==0.15.2
webrtc-models==0.3.0
+1 -1
View File
@@ -77,7 +77,7 @@ dependencies = [
"uv==0.11.21",
"voluptuous==0.15.2",
"voluptuous-serialize==2.7.0",
"voluptuous-openapi==0.3.0",
"voluptuous-openapi==0.4.1",
"yarl==1.24.2",
"webrtc-models==0.3.0",
"zeroconf==0.150.0",
+1 -1
View File
@@ -56,7 +56,7 @@ typing-extensions>=4.15.0,<5.0
ulid-transform==2.2.9
urllib3>=2.0
uv==0.11.21
voluptuous-openapi==0.3.0
voluptuous-openapi==0.4.1
voluptuous-serialize==2.7.0
voluptuous==0.15.2
webrtc-models==0.3.0
+2 -2
View File
@@ -2730,7 +2730,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==5.14.2
python-roborock==5.21.0
# homeassistant.components.smarttub
python-smarttub==0.0.47
@@ -2899,7 +2899,7 @@ renault-api==0.5.12
renson-endura-delta==1.7.2
# homeassistant.components.reolink
reolink-aio==0.21.2
reolink-aio==0.21.3
# homeassistant.components.radio_frequency
rf-protocols==4.3.0
+1 -1
View File
@@ -27,7 +27,7 @@ pytest-asyncio==1.4.0
pytest-aiohttp==1.1.1
pytest-cov==7.1.0
pytest-freezer==0.4.9
pytest-github-actions-annotate-failures==0.4.0
pytest-github-actions-annotate-failures==0.4.1
pytest-socket==0.8.0
pytest-sugar==1.1.1
pytest-timeout==2.4.0
-2
View File
@@ -913,7 +913,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"tesla_wall_connector",
"thermobeacon",
"thermopro",
"thermoworks_smoke",
"thethingsnetwork",
"thingspeak",
"thinkingcleaner",
@@ -1896,7 +1895,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
"tesla_wall_connector",
"thermobeacon",
"thermopro",
"thermoworks_smoke",
"thethingsnetwork",
"thingspeak",
"thinkingcleaner",
+2 -2
View File
@@ -257,8 +257,8 @@ WAVE_DEVICE_INFO = AirthingsDevice(
"temperature": 21.0,
"co2": 500.0,
"voc": 155.0,
"radon_1day_level": "very low",
"radon_longterm_level": "very low",
"radon_1day_level": "good",
"radon_longterm_level": "poor",
"pressure": 1020,
},
address="cc:cc:cc:cc:cc:cc",
@@ -853,12 +853,11 @@
}),
]),
'envoy_model_data': dict({
'ctmeter_consumption': None,
'ctmeter_consumption_phases': None,
'ctmeter_production': None,
'ctmeter_production_phases': None,
'ctmeter_storage': None,
'ctmeter_storage_phases': None,
'acb_inventory': None,
'acb_power': None,
'battery_aggregate': None,
'c6cc': None,
'collar': None,
'ctmeters': dict({
}),
'ctmeters_phases': dict({
@@ -1768,12 +1767,11 @@
}),
]),
'envoy_model_data': dict({
'ctmeter_consumption': None,
'ctmeter_consumption_phases': None,
'ctmeter_production': None,
'ctmeter_production_phases': None,
'ctmeter_storage': None,
'ctmeter_storage_phases': None,
'acb_inventory': None,
'acb_power': None,
'battery_aggregate': None,
'c6cc': None,
'collar': None,
'ctmeters': dict({
}),
'ctmeters_phases': dict({
@@ -1822,8 +1820,14 @@
]),
}),
'fixtures': dict({
'/admin/lib/acb_config': 'Testing request replies.',
'/admin/lib/acb_config_log': '{"headers":{"Hello":"World"},"code":200}',
'/admin/lib/network_display': 'Testing request replies.',
'/admin/lib/network_display_log': '{"headers":{"Hello":"World"},"code":200}',
'/admin/lib/tariff': 'Testing request replies.',
'/admin/lib/tariff_log': '{"headers":{"Hello":"World"},"code":200}',
'/admin/lib/wireless_display': 'Testing request replies.',
'/admin/lib/wireless_display_log': '{"headers":{"Hello":"World"},"code":200}',
'/api/v1/production': 'Testing request replies.',
'/api/v1/production/inverters': 'Testing request replies.',
'/api/v1/production/inverters_log': '{"headers":{"Hello":"World"},"code":200}',
@@ -1832,6 +1836,8 @@
'/home_log': '{"headers":{"Hello":"World"},"code":200}',
'/info': 'Testing request replies.',
'/info_log': '{"headers":{"Hello":"World"},"code":200}',
'/inventory.json?deleted=1': 'Testing request replies.',
'/inventory.json?deleted=1_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/ensemble/dry_contacts': 'Testing request replies.',
'/ivp/ensemble/dry_contacts_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/ensemble/generator': 'Testing request replies.',
@@ -1840,18 +1846,26 @@
'/ivp/ensemble/inventory_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/ensemble/power': 'Testing request replies.',
'/ivp/ensemble/power_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/ensemble/relay': 'Testing request replies.',
'/ivp/ensemble/relay_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/ensemble/secctrl': 'Testing request replies.',
'/ivp/ensemble/secctrl_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/ensemble/status': 'Testing request replies.',
'/ivp/ensemble/status_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/livedata/status': 'Testing request replies.',
'/ivp/livedata/status_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/meters': 'Testing request replies.',
'/ivp/meters/readings': 'Testing request replies.',
'/ivp/meters/readings_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/meters_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/pdm/device_data': 'Testing request replies.',
'/ivp/pdm/device_data_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/pdm/energy': 'Testing request replies.',
'/ivp/pdm/energy_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/sc/pvlimit': 'Testing request replies.',
'/ivp/sc/pvlimit_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/sc/sched': 'Testing request replies.',
'/ivp/sc/sched_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/ss/dry_contact_settings': 'Testing request replies.',
'/ivp/ss/dry_contact_settings_log': '{"headers":{"Hello":"World"},"code":200}',
'/ivp/ss/gen_config': 'Testing request replies.',
@@ -2727,12 +2741,11 @@
}),
]),
'envoy_model_data': dict({
'ctmeter_consumption': None,
'ctmeter_consumption_phases': None,
'ctmeter_production': None,
'ctmeter_production_phases': None,
'ctmeter_storage': None,
'ctmeter_storage_phases': None,
'acb_inventory': None,
'acb_power': None,
'battery_aggregate': None,
'c6cc': None,
'collar': None,
'ctmeters': dict({
}),
'ctmeters_phases': dict({
@@ -2781,9 +2794,18 @@
]),
}),
'fixtures': dict({
'/admin/lib/acb_config_log': dict({
'Error': "EnvoyError('Test')",
}),
'/admin/lib/network_display_log': dict({
'Error': "EnvoyError('Test')",
}),
'/admin/lib/tariff_log': dict({
'Error': "EnvoyError('Test')",
}),
'/admin/lib/wireless_display_log': dict({
'Error': "EnvoyError('Test')",
}),
'/api/v1/production/inverters_log': dict({
'Error': "EnvoyError('Test')",
}),
@@ -2796,6 +2818,9 @@
'/info_log': dict({
'Error': "EnvoyError('Test')",
}),
'/inventory.json?deleted=1_log': dict({
'Error': "EnvoyError('Test')",
}),
'/ivp/ensemble/dry_contacts_log': dict({
'Error': "EnvoyError('Test')",
}),
@@ -2808,12 +2833,18 @@
'/ivp/ensemble/power_log': dict({
'Error': "EnvoyError('Test')",
}),
'/ivp/ensemble/relay_log': dict({
'Error': "EnvoyError('Test')",
}),
'/ivp/ensemble/secctrl_log': dict({
'Error': "EnvoyError('Test')",
}),
'/ivp/ensemble/status_log': dict({
'Error': "EnvoyError('Test')",
}),
'/ivp/livedata/status_log': dict({
'Error': "EnvoyError('Test')",
}),
'/ivp/meters/readings_log': dict({
'Error': "EnvoyError('Test')",
}),
@@ -2823,9 +2854,15 @@
'/ivp/pdm/device_data_log': dict({
'Error': "EnvoyError('Test')",
}),
'/ivp/pdm/energy_log': dict({
'Error': "EnvoyError('Test')",
}),
'/ivp/sc/pvlimit_log': dict({
'Error': "EnvoyError('Test')",
}),
'/ivp/sc/sched_log': dict({
'Error': "EnvoyError('Test')",
}),
'/ivp/ss/dry_contact_settings_log': dict({
'Error': "EnvoyError('Test')",
}),
@@ -3711,12 +3748,11 @@
}),
]),
'envoy_model_data': dict({
'ctmeter_consumption': None,
'ctmeter_consumption_phases': None,
'ctmeter_production': None,
'ctmeter_production_phases': None,
'ctmeter_storage': None,
'ctmeter_storage_phases': None,
'acb_inventory': None,
'acb_power': None,
'battery_aggregate': None,
'c6cc': None,
'collar': None,
'ctmeters': dict({
}),
'ctmeters_phases': dict({
@@ -19745,59 +19781,16 @@
}),
]),
'envoy_model_data': dict({
'ctmeter_consumption': dict({
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
'repr': "EnvoyMeterData(eid='100000020', timestamp=1708006120, energy_delivered=21234, energy_received=22345, active_power=101, power_factor=0.21, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='net-consumption', metering_status='normal', status_flags=[])",
'acb_inventory': None,
'acb_power': None,
'battery_aggregate': None,
'c6cc': dict({
'__type': "<class 'pyenphase.models.c6combiner.EnvoyC6CC'>",
'repr': "EnvoyC6CC(admin_state=82, admin_state_str='ENCMN_C6_CC_READY', firmware_loaded_date=1752945451, firmware_version='0.1.20-D1', installed_date=1752945451, last_report_date=1752945451, communicating=True, part_number='800-02403-r08', serial_number='482523040549', dmir_version='0.1.20-D1')",
}),
'ctmeter_consumption_phases': dict({
'L1': dict({
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
'repr': "EnvoyMeterData(eid='100000021', timestamp=1708006121, energy_delivered=212341, energy_received=223451, active_power=21, power_factor=0.22, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='net-consumption', metering_status='normal', status_flags=[])",
}),
'L2': dict({
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
'repr': "EnvoyMeterData(eid='100000022', timestamp=1708006122, energy_delivered=212342, energy_received=223452, active_power=31, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='net-consumption', metering_status='normal', status_flags=[])",
}),
'L3': dict({
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
'repr': "EnvoyMeterData(eid='100000023', timestamp=1708006123, energy_delivered=212343, energy_received=223453, active_power=51, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='net-consumption', metering_status='normal', status_flags=[])",
}),
}),
'ctmeter_production': dict({
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
'repr': "EnvoyMeterData(eid='100000010', timestamp=1708006110, energy_delivered=11234, energy_received=12345, active_power=100, power_factor=0.11, voltage=111, current=0.2, frequency=50.1, state='enabled', measurement_type='production', metering_status='normal', status_flags=['production-imbalance', 'power-on-unused-phase'])",
}),
'ctmeter_production_phases': dict({
'L1': dict({
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
'repr': "EnvoyMeterData(eid='100000011', timestamp=1708006111, energy_delivered=112341, energy_received=123451, active_power=20, power_factor=0.12, voltage=111, current=0.2, frequency=50.1, state='enabled', measurement_type='production', metering_status='normal', status_flags=['production-imbalance'])",
}),
'L2': dict({
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
'repr': "EnvoyMeterData(eid='100000012', timestamp=1708006112, energy_delivered=112342, energy_received=123452, active_power=30, power_factor=0.13, voltage=111, current=0.2, frequency=50.1, state='enabled', measurement_type='production', metering_status='normal', status_flags=['power-on-unused-phase'])",
}),
'L3': dict({
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
'repr': "EnvoyMeterData(eid='100000013', timestamp=1708006113, energy_delivered=112343, energy_received=123453, active_power=50, power_factor=0.14, voltage=111, current=0.2, frequency=50.1, state='enabled', measurement_type='production', metering_status='normal', status_flags=[])",
}),
}),
'ctmeter_storage': dict({
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
'repr': "EnvoyMeterData(eid='100000030', timestamp=1708006120, energy_delivered=31234, energy_received=32345, active_power=103, power_factor=0.23, voltage=113, current=0.4, frequency=50.3, state='enabled', measurement_type='storage', metering_status='normal', status_flags=[])",
}),
'ctmeter_storage_phases': dict({
'L1': dict({
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
'repr': "EnvoyMeterData(eid='100000031', timestamp=1708006121, energy_delivered=312341, energy_received=323451, active_power=22, power_factor=0.32, voltage=113, current=0.4, frequency=50.3, state='enabled', measurement_type='storage', metering_status='normal', status_flags=[])",
}),
'L2': dict({
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
'repr': "EnvoyMeterData(eid='100000032', timestamp=1708006122, energy_delivered=312342, energy_received=323452, active_power=33, power_factor=0.23, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='storage', metering_status='normal', status_flags=[])",
}),
'L3': dict({
'__type': "<class 'pyenphase.models.meters.EnvoyMeterData'>",
'repr': "EnvoyMeterData(eid='100000033', timestamp=1708006123, energy_delivered=312343, energy_received=323453, active_power=53, power_factor=0.24, voltage=112, current=0.3, frequency=50.2, state='enabled', measurement_type='storage', metering_status='normal', status_flags=[])",
}),
'collar': dict({
'__type': "<class 'pyenphase.models.collar.EnvoyCollar'>",
'repr': "EnvoyCollar(admin_state=88, admin_state_str='ENCMN_MDE_ON_GRID', firmware_loaded_date=1752939759, firmware_version='3.0.6-D0', installed_date=1752939759, last_report_date=1752939759, communicating=True, mid_state='close', grid_state='on_grid', part_number='865-00400-r22', serial_number='482520020939', temperature=42, temperature_unit='C', control_error=0, collar_state='Installed')",
}),
'ctmeters': dict({
'backfeed': dict({
+21 -71
View File
@@ -607,7 +607,7 @@ async def test_entity_id_preserved_on_upgrade_when_in_storage(
ent_reg_entry = entity_registry.async_get_or_create(
Platform.BINARY_SENSOR,
DOMAIN,
"11:22:33:44:55:AA/0/binary_sensor/my",
"11:22:33:44:55:AA-binary_sensor-my",
)
entity_registry.async_update_entity(
ent_reg_entry.entity_id,
@@ -1161,58 +1161,6 @@ async def test_entity_id_with_empty_sub_device_name(
assert hass.states.get("binary_sensor.main_device_sensor") is not None
async def test_legacy_unique_id_migrated_to_v3_sub_device(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_client: APIClient,
mock_esphome_device: MockESPHomeDeviceType,
) -> None:
"""Test a legacy sub-device unique id is migrated to the version 3 format."""
sub_devices = [
SubDeviceInfo(device_id=22222222, name="kitchen_controller", area_id=0),
]
device_info = {"name": "test", "devices": sub_devices}
entity_info = [
BinarySensorInfo(
object_id="temperature",
key=1,
name="Temperature",
device_id=22222222,
),
]
states = [BinarySensorState(key=1, state=True, missing_state=False)]
# Seed a registry entry in the legacy format with the @device_id suffix
legacy_entry = entity_registry.async_get_or_create(
Platform.BINARY_SENSOR,
DOMAIN,
"11:22:33:44:55:AA-binary_sensor-temperature@22222222",
suggested_object_id="kitchen_controller_temperature",
)
await mock_esphome_device(
mock_client=mock_client,
device_info=device_info,
entity_info=entity_info,
states=states,
)
entity_entry = entity_registry.async_get(legacy_entry.entity_id)
assert entity_entry is not None
# The legacy id is renamed to version 3, keeping the same entity
assert (
entity_entry.unique_id == "11:22:33:44:55:AA/22222222/binary_sensor/Temperature"
)
assert (
entity_registry.async_get_entity_id(
Platform.BINARY_SENSOR,
DOMAIN,
"11:22:33:44:55:AA-binary_sensor-temperature@22222222",
)
is None
)
async def test_unique_id_migration_when_entity_moves_between_devices(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
@@ -1256,8 +1204,8 @@ async def test_unique_id_migration_when_entity_moves_between_devices(
entity_entry = entity_registry.async_get("binary_sensor.test_temperature")
assert entity_entry is not None
initial_unique_id = entity_entry.unique_id
# Main device entities use device_id 0 in the unique id
assert "/0/" in initial_unique_id
# Initial unique_id should not have @device_id suffix since it's on main device
assert "@" not in initial_unique_id
# Add sub-device to device info
sub_devices = [
@@ -1312,8 +1260,8 @@ async def test_unique_id_migration_when_entity_moves_between_devices(
# Wait for entity to be updated
await hass.async_block_till_done()
# The entity_id doesn't change when moving between devices,
# only the device segment of the unique_id changes
# The entity_id doesn't change when moving between devices
# Only the unique_id gets updated with @device_id suffix
state = hass.states.get("binary_sensor.test_temperature")
assert state is not None
@@ -1321,8 +1269,9 @@ async def test_unique_id_migration_when_entity_moves_between_devices(
entity_entry = entity_registry.async_get("binary_sensor.test_temperature")
assert entity_entry is not None
# Unique ID device segment should now be the sub-device id
expected_unique_id = initial_unique_id.replace("/0/", "/22222222/")
# Unique ID should have been migrated to include @device_id
# This is done by our build_device_unique_id wrapper
expected_unique_id = f"{initial_unique_id}@22222222"
assert entity_entry.unique_id == expected_unique_id
# Entity should now be associated with the sub-device
@@ -1382,8 +1331,8 @@ async def test_unique_id_migration_sub_device_to_main_device(
)
assert entity_entry is not None
initial_unique_id = entity_entry.unique_id
# Sub-device entities carry the sub-device id in the unique id
assert "/22222222/" in initial_unique_id
# Initial unique_id should have @device_id suffix since it's on sub-device
assert "@22222222" in initial_unique_id
# Update entity info - move to main device
new_entity_info = [
@@ -1416,8 +1365,8 @@ async def test_unique_id_migration_sub_device_to_main_device(
)
assert entity_entry is not None
# Unique ID device segment should now be the main device id 0
expected_unique_id = initial_unique_id.replace("/22222222/", "/0/")
# Unique ID should have been migrated to remove @device_id suffix
expected_unique_id = initial_unique_id.replace("@22222222", "")
assert entity_entry.unique_id == expected_unique_id
# Entity should now be associated with the main device
@@ -1478,8 +1427,8 @@ async def test_unique_id_migration_between_sub_devices(
)
assert entity_entry is not None
initial_unique_id = entity_entry.unique_id
# Sub-device entities carry the sub-device id in the unique id
assert "/22222222/" in initial_unique_id
# Initial unique_id should have @22222222 suffix
assert "@22222222" in initial_unique_id
# Update entity info - move to second sub-device
new_entity_info = [
@@ -1512,8 +1461,8 @@ async def test_unique_id_migration_between_sub_devices(
)
assert entity_entry is not None
# Unique ID device segment should have moved from 22222222 to 33333333
expected_unique_id = initial_unique_id.replace("/22222222/", "/33333333/")
# Unique ID should have been migrated from @22222222 to @33333333
expected_unique_id = initial_unique_id.replace("@22222222", "@33333333")
assert entity_entry.unique_id == expected_unique_id
# Entity should now be associated with the second sub-device
@@ -1575,8 +1524,8 @@ async def test_entity_device_id_rename_in_yaml(
entity_entry = entity_registry.async_get("binary_sensor.old_device_sensor")
assert entity_entry is not None
initial_unique_id = entity_entry.unique_id
# Sub-device entities carry the sub-device id in the unique id
assert "/11111111/" in initial_unique_id
# Should have @11111111 suffix
assert "@11111111" in initial_unique_id
# Simulate user renaming device_id in YAML config
# The device_id hash changes from 11111111 to 99999999
@@ -1638,8 +1587,9 @@ async def test_entity_device_id_rename_in_yaml(
entity_entry = entity_registry.async_get("binary_sensor.renamed_device_sensor")
assert entity_entry is not None
# Unique ID device segment should have the new device_id
expected_unique_id = initial_unique_id.replace("/11111111/", "/99999999/")
# Unique ID should have the new device_id
base_unique_id = initial_unique_id.replace("@11111111", "")
expected_unique_id = f"{base_unique_id}@99999999"
assert entity_entry.unique_id == expected_unique_id
# Entity should be associated with the new device
+8 -57
View File
@@ -21,53 +21,6 @@ from homeassistant.helpers.service_info.esphome import ESPHomeServiceInfo
from .conftest import MockGenericDeviceEntryType
async def test_migrate_entity_unique_id(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test a legacy unique id is migrated to the version 3 format."""
entity_registry.async_get_or_create(
SENSOR_DOMAIN,
DOMAIN,
"11:22:33:44:55:AA-sensor-mysensor",
suggested_object_id="my_sensor",
disabled_by=None,
)
entity_info = [
SensorInfo(
object_id="mysensor",
key=1,
name="my sensor",
entity_category=ESPHomeEntityCategory.DIAGNOSTIC,
icon="mdi:leaf",
)
]
states = [SensorState(key=1, state=50)]
user_service = []
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
)
state = hass.states.get("sensor.my_sensor")
assert state is not None
assert state.state == "50"
entry = entity_registry.async_get("sensor.my_sensor")
assert entry is not None
# The legacy unique id should have been renamed to the version 3 format,
# keeping the entity (and its entity_id) instead of creating a new one
assert entry.unique_id == "11:22:33:44:55:AA/0/sensor/my sensor"
assert (
entity_registry.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, "11:22:33:44:55:AA-sensor-mysensor"
)
is None
)
async def test_migrate_entity_unique_id_downgrade_upgrade(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
@@ -75,20 +28,18 @@ async def test_migrate_entity_unique_id_downgrade_upgrade(
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test unique id migration prefers the original entity on downgrade upgrade."""
# The original entity, already in the version 3 format
entity_registry.async_get_or_create(
SENSOR_DOMAIN,
DOMAIN,
"11:22:33:44:55:AA/0/sensor/my sensor",
suggested_object_id="new_sensor",
"my_sensor",
suggested_object_id="old_sensor",
disabled_by=None,
)
# A duplicate left behind in the legacy format by a downgrade
entity_registry.async_get_or_create(
SENSOR_DOMAIN,
DOMAIN,
"11:22:33:44:55:AA-sensor-mysensor",
suggested_object_id="old_sensor",
suggested_object_id="new_sensor",
disabled_by=None,
)
entity_info = [
@@ -113,17 +64,17 @@ async def test_migrate_entity_unique_id_downgrade_upgrade(
assert state.state == "50"
entry = entity_registry.async_get("sensor.new_sensor")
assert entry is not None
# Confirm we did not touch the legacy entity that was created
# Confirm we did not touch the entity that was created
# on downgrade so when they upgrade again they can delete the
# entity that was only created on downgrade and they keep
# the original one.
assert (
entity_registry.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, "11:22:33:44:55:AA-sensor-mysensor"
)
entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, "my_sensor")
is not None
)
assert entry.unique_id == "11:22:33:44:55:AA/0/sensor/my sensor"
# Note that ESPHome includes the EntityInfo type in the unique id
# as this is not a 1:1 mapping to the entity platform (ie. text_sensor)
assert entry.unique_id == "11:22:33:44:55:AA-sensor-mysensor"
async def test_discover_zwave() -> None:
+4 -8
View File
@@ -139,15 +139,13 @@ async def test_device_conflict_migration(
ent_reg_entry = entity_registry.async_get("binary_sensor.test_my_binary_sensor")
assert ent_reg_entry
assert (
ent_reg_entry.unique_id == "11:22:33:44:55:AA/0/binary_sensor/my binary_sensor"
)
assert ent_reg_entry.unique_id == "11:22:33:44:55:AA-binary_sensor-mybinary_sensor"
entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert entries is not None
for entry in entries:
assert entry.unique_id.startswith("11:22:33:44:55:AA/")
assert entry.unique_id.startswith("11:22:33:44:55:AA-")
disconnect_done = hass.loop.create_future()
async def async_disconnect(*args, **kwargs) -> None:
@@ -203,16 +201,14 @@ async def test_device_conflict_migration(
assert mock_config_entry.unique_id == "11:22:33:44:55:ab"
ent_reg_entry = entity_registry.async_get("binary_sensor.test_my_binary_sensor")
assert ent_reg_entry
assert (
ent_reg_entry.unique_id == "11:22:33:44:55:AB/0/binary_sensor/my binary_sensor"
)
assert ent_reg_entry.unique_id == "11:22:33:44:55:AB-binary_sensor-mybinary_sensor"
entries = er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
assert entries is not None
for entry in entries:
assert entry.unique_id.startswith("11:22:33:44:55:AB/")
assert entry.unique_id.startswith("11:22:33:44:55:AB-")
dev_entry = device_registry.async_get_device(
identifiers={}, connections={(dr.CONNECTION_NETWORK_MAC, "11:22:33:44:55:ab")}
+3 -3
View File
@@ -129,7 +129,7 @@ async def test_generic_numeric_sensor_with_entity_category_and_icon(
assert entry is not None
# Note that ESPHome includes the EntityInfo type in the unique id
# as this is not a 1:1 mapping to the entity platform (ie. text_sensor)
assert entry.unique_id == "11:22:33:44:55:AA/0/sensor/my sensor"
assert entry.unique_id == "11:22:33:44:55:AA-sensor-mysensor"
assert entry.entity_category is EntityCategory.DIAGNOSTIC
@@ -168,7 +168,7 @@ async def test_generic_numeric_sensor_state_class_measurement(
assert entry is not None
# Note that ESPHome includes the EntityInfo type in the unique id
# as this is not a 1:1 mapping to the entity platform (ie. text_sensor)
assert entry.unique_id == "11:22:33:44:55:AA/0/sensor/my sensor"
assert entry.unique_id == "11:22:33:44:55:AA-sensor-mysensor"
assert entry.entity_category is None
@@ -205,7 +205,7 @@ async def test_generic_numeric_sensor_state_class_measurement_angle(
assert entry is not None
# Note that ESPHome includes the EntityInfo type in the unique id
# as this is not a 1:1 mapping to the entity platform (ie. text_sensor)
assert entry.unique_id == "11:22:33:44:55:AA/0/sensor/my sensor"
assert entry.unique_id == "11:22:33:44:55:AA-sensor-mysensor"
assert entry.entity_category is None
@@ -107,7 +107,7 @@
'supported_features': 0,
'translation_key': 'c6h6',
'unique_id': '123-c6h6',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.home_benzene-state]
@@ -116,7 +116,7 @@
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by GIOŚ',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home Benzene',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_benzene',
@@ -165,7 +165,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-co',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.home_carbon_monoxide-state]
@@ -175,7 +175,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_monoxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home Carbon monoxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_carbon_monoxide',
@@ -224,7 +224,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-no2',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.home_nitrogen_dioxide-state]
@@ -234,7 +234,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'nitrogen_dioxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home Nitrogen dioxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_nitrogen_dioxide',
@@ -352,7 +352,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-no',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.home_nitrogen_monoxide-state]
@@ -362,7 +362,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'nitrogen_monoxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home Nitrogen monoxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_nitrogen_monoxide',
@@ -411,7 +411,7 @@
'supported_features': 0,
'translation_key': 'nox',
'unique_id': '123-nox',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.home_nitrogen_oxides-state]
@@ -420,7 +420,7 @@
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by GIOŚ',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home Nitrogen oxides',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_nitrogen_oxides',
@@ -469,7 +469,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-o3',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.home_ozone-state]
@@ -479,7 +479,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'ozone',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home Ozone',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_ozone',
@@ -597,7 +597,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-pm10',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.home_pm10-state]
@@ -607,7 +607,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm10',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home PM10',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_pm10',
@@ -725,7 +725,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-pm25',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.home_pm2_5-state]
@@ -735,7 +735,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_pm2_5',
@@ -853,7 +853,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123-so2',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.home_sulphur_dioxide-state]
@@ -863,7 +863,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'sulphur_dioxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home Sulphur dioxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_sulphur_dioxide',
+79 -4
View File
@@ -107,12 +107,12 @@ async def test_job_manager_ws_updates(
job_data: Job | None = None
@callback
def mock_subcription_callback(job: Job) -> None:
def mock_subscription_callback(job: Job) -> None:
nonlocal job_data
job_data = job
subscription = JobSubscription(
mock_subcription_callback, name="test_job", reference="test"
mock_subscription_callback, name="test_job", reference="test"
)
unsubscribe = data_coordinator.jobs.subscribe(subscription)
@@ -318,11 +318,11 @@ async def test_job_manager_reload_on_supervisor_restart(
job_data: Job | None = None
@callback
def mock_subcription_callback(job: Job) -> None:
def mock_subscription_callback(job: Job) -> None:
nonlocal job_data
job_data = job
subscription = JobSubscription(mock_subcription_callback, name="test_job")
subscription = JobSubscription(mock_subscription_callback, name="test_job")
data_coordinator.jobs.subscribe(subscription)
# Send supervisor restart signal
@@ -347,3 +347,78 @@ async def test_job_manager_reload_on_supervisor_restart(
assert job_data.reference == "test"
assert job_data.done is True
assert not data_coordinator.jobs.current_jobs
@pytest.mark.usefixtures("all_setup_requests")
async def test_subscribe_returns_unsubscribe_when_job_already_matches(
hass: HomeAssistant,
jobs_info: AsyncMock,
hass_supervisor_ws_client: WebSocketGenerator,
) -> None:
"""Test subscribe returns a working unsubscribe even if a job already matches."""
jobs_info.return_value = JobsInfo(
ignore_conditions=[],
jobs=[
Job(
name="test_job",
reference="test",
uuid=uuid4(),
progress=0,
stage=None,
done=False,
errors=[],
created=datetime.now(), # pylint: disable=home-assistant-enforce-naive-now
extra=None,
child_jobs=[],
)
],
)
result = await async_setup_component(hass, DOMAIN, {})
assert result
client = await hass_supervisor_ws_client()
data_coordinator: HassioMainDataUpdateCoordinator = hass.data[MAIN_COORDINATOR]
received: list[Job] = []
@callback
def mock_subscription_callback(job: Job) -> None:
received.append(job)
subscription = JobSubscription(mock_subscription_callback, name="test_job")
unsubscribe = data_coordinator.jobs.subscribe(subscription)
# Existing matching job is delivered immediately, and a callable unsubscribe
# is returned (not the None result of the callback)
assert len(received) == 1
assert received[0].name == "test_job"
assert callable(unsubscribe)
# After unsubscribing, a new matching job update is no longer delivered
unsubscribe()
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "job",
"data": {
"name": "test_job",
"reference": "test",
"uuid": uuid4().hex,
"progress": 50,
"stage": None,
"done": False,
"errors": [],
"created": datetime.now().isoformat(), # pylint: disable=home-assistant-enforce-naive-now
"extra": None,
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
assert len(received) == 1
@@ -96,5 +96,7 @@
"8646": 150,
"11005": 35.5,
"7171": 1,
"680": 0
"680": 0,
"11019": 45,
"11020": 0
}
@@ -66,6 +66,8 @@
'11010': 52.3,
'11011': 85,
'11016': 0,
'11019': 45,
'11020': 0,
'11034': 100,
'11042': 32.1,
'142': 1.79,
@@ -6635,6 +6635,122 @@
'state': 'unavailable',
})
# ---
# name: test_sensor[2][sensor.cms_sf2000_remaining_charging_time-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: '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.cms_sf2000_remaining_charging_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Remaining charging time',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Remaining charging time',
'platform': 'indevolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'remaining_charging_time',
'unique_id': 'SolidFlex2000-87654321_11019',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_sensor[2][sensor.cms_sf2000_remaining_charging_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'duration',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'CMS-SF2000 Remaining charging time',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.cms_sf2000_remaining_charging_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
})
# ---
# name: test_sensor[2][sensor.cms_sf2000_remaining_discharging_time-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
<SensorEntityCapabilityAttribute.STATE_CLASS: '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.cms_sf2000_remaining_discharging_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Remaining discharging time',
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Remaining discharging time',
'platform': 'indevolt',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'remaining_discharging_time',
'unique_id': 'SolidFlex2000-87654321_11020',
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_sensor[2][sensor.cms_sf2000_remaining_discharging_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'duration',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'CMS-SF2000 Remaining discharging time',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.cms_sf2000_remaining_discharging_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unavailable',
})
# ---
# name: test_sensor[2][sensor.cms_sf2000_total_ac_input_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
+62
View File
@@ -157,6 +157,68 @@ async def test_inverter_sensor_temperature_availability(
assert hass.states.get("sensor.bk1600_inverter_temperature").state == "0"
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("generation", [2], indirect=True)
async def test_remaining_time_sensor_charge_state_availability(
hass: HomeAssistant,
mock_indevolt: AsyncMock,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test remaining time sensors are only available in the matching charge/discharge state."""
with patch("homeassistant.components.indevolt.PLATFORMS", [Platform.SENSOR]):
await setup_integration(hass, mock_config_entry)
# Default charge / discharge state is static (1000), both sensors should be unavailable
assert (
hass.states.get("sensor.cms_sf2000_remaining_charging_time").state
== STATE_UNAVAILABLE
)
assert (
hass.states.get("sensor.cms_sf2000_remaining_discharging_time").state
== STATE_UNAVAILABLE
)
# Switch to charging (1001), only remaining charging time should be available
mock_indevolt.fetch_data.return_value[IndevoltBattery.CHARGE_DISCHARGE_STATE] = 1001
freezer.tick(delta=timedelta(seconds=SCAN_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get("sensor.cms_sf2000_remaining_charging_time").state == "45"
assert (
hass.states.get("sensor.cms_sf2000_remaining_discharging_time").state
== STATE_UNAVAILABLE
)
# Switch to discharging (1002), only remaining discharging time should be available
mock_indevolt.fetch_data.return_value[IndevoltBattery.CHARGE_DISCHARGE_STATE] = 1002
freezer.tick(delta=timedelta(seconds=SCAN_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (
hass.states.get("sensor.cms_sf2000_remaining_charging_time").state
== STATE_UNAVAILABLE
)
assert hass.states.get("sensor.cms_sf2000_remaining_discharging_time").state == "0"
# Switch back to static (1000), both sensors become unavailable again
mock_indevolt.fetch_data.return_value[IndevoltBattery.CHARGE_DISCHARGE_STATE] = 1000
freezer.tick(delta=timedelta(seconds=SCAN_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (
hass.states.get("sensor.cms_sf2000_remaining_charging_time").state
== STATE_UNAVAILABLE
)
assert (
hass.states.get("sensor.cms_sf2000_remaining_discharging_time").state
== STATE_UNAVAILABLE
)
# In individual tests, you can override the mock behavior
async def test_battery_pack_filtering(
hass: HomeAssistant,
+28 -28
View File
@@ -96,7 +96,7 @@
'supported_features': 0,
'translation_key': 'bme280_humidity',
'unique_id': 'aa:bb:cc:dd:ee:ff-bme280_humidity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_bme280_humidity-state]
@@ -105,7 +105,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor BME280 humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_bme280_humidity',
@@ -502,7 +502,7 @@
'supported_features': 0,
'translation_key': 'dht22_humidity',
'unique_id': 'aa:bb:cc:dd:ee:ff-dht22_humidity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_dht22_humidity-state]
@@ -511,7 +511,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor DHT22 humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_dht22_humidity',
@@ -676,7 +676,7 @@
'supported_features': 0,
'translation_key': 'heca_humidity',
'unique_id': 'aa:bb:cc:dd:ee:ff-heca_humidity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_heca_humidity-state]
@@ -685,7 +685,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor HECA humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_heca_humidity',
@@ -792,7 +792,7 @@
'supported_features': 0,
'translation_key': 'mhz14a_carbon_dioxide',
'unique_id': 'aa:bb:cc:dd:ee:ff-mhz14a_carbon_dioxide',
'unit_of_measurement': 'ppm',
'unit_of_measurement': <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide-state]
@@ -801,7 +801,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'carbon_dioxide',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor MH-Z14A carbon dioxide',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'ppm',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PARTS_PER_MILLION: 'ppm'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_mh_z14a_carbon_dioxide',
@@ -966,7 +966,7 @@
'supported_features': 0,
'translation_key': 'pmsx003_pm1',
'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p0',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm1-state]
@@ -975,7 +975,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm1',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor PMSx003 PM1',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm1',
@@ -1024,7 +1024,7 @@
'supported_features': 0,
'translation_key': 'pmsx003_pm10',
'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p1',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm10-state]
@@ -1033,7 +1033,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm10',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor PMSx003 PM10',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm10',
@@ -1082,7 +1082,7 @@
'supported_features': 0,
'translation_key': 'pmsx003_pm25',
'unique_id': 'aa:bb:cc:dd:ee:ff-pms_p2',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_pmsx003_pm2_5-state]
@@ -1091,7 +1091,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor PMSx003 PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_pmsx003_pm2_5',
@@ -1256,7 +1256,7 @@
'supported_features': 0,
'translation_key': 'sds011_pm10',
'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p1',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_sds011_pm10-state]
@@ -1265,7 +1265,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm10',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor SDS011 PM10',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sds011_pm10',
@@ -1314,7 +1314,7 @@
'supported_features': 0,
'translation_key': 'sds011_pm25',
'unique_id': 'aa:bb:cc:dd:ee:ff-sds011_p2',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_sds011_pm2_5-state]
@@ -1323,7 +1323,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor SDS011 PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sds011_pm2_5',
@@ -1372,7 +1372,7 @@
'supported_features': 0,
'translation_key': 'sht3x_humidity',
'unique_id': 'aa:bb:cc:dd:ee:ff-sht3x_humidity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_sht3x_humidity-state]
@@ -1381,7 +1381,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor SHT3X humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sht3x_humidity',
@@ -1662,7 +1662,7 @@
'supported_features': 0,
'translation_key': 'sps30_pm1',
'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p0',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm1-state]
@@ -1671,7 +1671,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm1',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor SPS30 PM1',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sps30_pm1',
@@ -1720,7 +1720,7 @@
'supported_features': 0,
'translation_key': 'sps30_pm10',
'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p1',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm10-state]
@@ -1729,7 +1729,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm10',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor SPS30 PM10',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sps30_pm10',
@@ -1778,7 +1778,7 @@
'supported_features': 0,
'translation_key': 'sps30_pm25',
'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p2',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm2_5-state]
@@ -1787,7 +1787,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor SPS30 PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sps30_pm2_5',
@@ -1836,7 +1836,7 @@
'supported_features': 0,
'translation_key': 'sps30_pm4',
'unique_id': 'aa:bb:cc:dd:ee:ff-sps30_p4',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor[sensor.nettigo_air_monitor_sps30_pm4-state]
@@ -1845,7 +1845,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm4',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Nettigo Air Monitor SPS30 PM4',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.nettigo_air_monitor_sps30_pm4',
@@ -35,7 +35,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'ECC9FFE0E574_humidity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_sensors[sensor.prana_recuperator_humidity-state]
@@ -44,7 +44,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'humidity',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'PRANA RECUPERATOR Humidity',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.prana_recuperator_humidity',
+1 -1
View File
@@ -1076,7 +1076,7 @@ async def test_privacy_mode_change_callback(
def register_callback(
self, callback_id: str, callback: Callable[[], None], *args, **key_args
) -> None:
if callback_id == "privacy_mode_change":
if callback_id == "privacy_mode_change_623":
self.callback_func = callback
callback_mock = callback_mock_class()
+18 -1
View File
@@ -4,9 +4,10 @@ import pytest
from roborock.exceptions import RoborockTimeout
from homeassistant.components.number import ATTR_VALUE, SERVICE_SET_VALUE
from homeassistant.const import Platform
from homeassistant.const import STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_component import async_update_entity
from .conftest import FakeDevice
@@ -49,6 +50,22 @@ async def test_update_sound_volume(
assert state.state == "3.0"
async def test_volume_unknown_value(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
fake_vacuum: FakeDevice,
) -> None:
"""Test the entity reports unknown when the trait value is None."""
assert fake_vacuum.v1_properties is not None
fake_vacuum.v1_properties.sound_volume.volume = None
await async_update_entity(hass, "number.roborock_s7_maxv_volume")
state = hass.states.get("number.roborock_s7_maxv_volume")
assert state is not None
assert state.state == STATE_UNKNOWN
async def test_volume_update_failed(
hass: HomeAssistant,
setup_entry: MockConfigEntry,
+54 -3
View File
@@ -1,21 +1,22 @@
"""Test Roborock Time platform."""
from collections.abc import Callable
from datetime import time
from datetime import time, timedelta
from typing import Any
from freezegun.api import FrozenDateTimeFactory
import pytest
import roborock
from roborock.data import DnDTimer, RoborockBaseTimer, ValleyElectricityTimer
from homeassistant.components.time import SERVICE_SET_VALUE
from homeassistant.const import Platform
from homeassistant.const import STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .conftest import FakeDevice
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.fixture
@@ -127,3 +128,53 @@ async def test_update_failure(
target={"entity_id": entity_id},
)
assert fake_vacuum.v1_properties.dnd.set_dnd_timer.call_count == 1
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
("entity_id", "trait", "missing_attribute"),
[
(
"time.roborock_s7_maxv_do_not_disturb_begin",
lambda x: x.v1_properties.dnd,
"start_hour",
),
(
"time.roborock_s7_maxv_do_not_disturb_begin",
lambda x: x.v1_properties.dnd,
"start_minute",
),
(
"time.roborock_s7_maxv_do_not_disturb_end",
lambda x: x.v1_properties.dnd,
"end_hour",
),
(
"time.roborock_s7_maxv_off_peak_start",
lambda x: x.v1_properties.valley_electricity_timer,
"start_minute",
),
],
)
async def test_missing_value(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
setup_entry: MockConfigEntry,
fake_vacuum: FakeDevice,
entity_id: str,
trait: Callable[[FakeDevice], Any],
missing_attribute: str,
) -> None:
"""Test that a missing time component reports as unknown instead of raising."""
state = hass.states.get(entity_id)
assert state is not None
assert state.state != STATE_UNKNOWN
setattr(trait(fake_vacuum), missing_attribute, None)
freezer.tick(timedelta(seconds=31))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_UNKNOWN
@@ -293,7 +293,7 @@
'supported_features': 0,
'translation_key': 'left_slot_intensity',
'unique_id': '123456789ABC-cury:0-left_slot_intensity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_device[cury_gen4][number.test_name_left_slot_intensity-state]
@@ -304,7 +304,7 @@
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.SLIDER: 'slider'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'number.test_name_left_slot_intensity',
@@ -353,7 +353,7 @@
'supported_features': 0,
'translation_key': 'right_slot_intensity',
'unique_id': '123456789ABC-cury:0-right_slot_intensity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_device[cury_gen4][number.test_name_right_slot_intensity-state]
@@ -364,7 +364,7 @@
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.SLIDER: 'slider'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'number.test_name_right_slot_intensity',
@@ -479,7 +479,7 @@
'supported_features': 0,
'translation_key': 'left_slot_level',
'unique_id': '123456789ABC-cury:0-cury_left_level',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_device[cury_gen4][sensor.test_name_left_slot_level-state]
@@ -487,7 +487,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test name Left slot level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_name_left_slot_level',
@@ -583,7 +583,7 @@
'supported_features': 0,
'translation_key': 'right_slot_level',
'unique_id': '123456789ABC-cury:0-cury_right_level',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_device[cury_gen4][sensor.test_name_right_slot_level-state]
@@ -591,7 +591,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test name Right slot level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_name_right_slot_level',
@@ -99,7 +99,7 @@
'supported_features': 0,
'translation_key': 'valve_position',
'unique_id': '123456789ABC-blutrv:200-valve_position',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_blu_trv_number_entity[number.trv_name_valve_position-state]
@@ -110,7 +110,7 @@
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.SLIDER: 'slider'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'number.trv_name_valve_position',
@@ -159,7 +159,7 @@
'supported_features': 0,
'translation_key': 'left_slot_intensity',
'unique_id': '123456789ABC-cury:0-left_slot_intensity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_cury_number_entity[number.test_name_left_slot_intensity-state]
@@ -170,7 +170,7 @@
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.SLIDER: 'slider'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'number.test_name_left_slot_intensity',
@@ -219,7 +219,7 @@
'supported_features': 0,
'translation_key': 'right_slot_intensity',
'unique_id': '123456789ABC-cury:0-right_slot_intensity',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_cury_number_entity[number.test_name_right_slot_intensity-state]
@@ -230,7 +230,7 @@
<NumberEntityCapabilityAttribute.MIN: 'min'>: 0,
<NumberEntityCapabilityAttribute.MODE: 'mode'>: <NumberMode.SLIDER: 'slider'>,
<NumberEntityCapabilityAttribute.STEP: 'step'>: 1,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'number.test_name_right_slot_intensity',
@@ -35,7 +35,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '123456789ABC-blutrv:200-blutrv_battery',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_blu_trv_sensor_entity[sensor.trv_name_battery-state]
@@ -44,7 +44,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'battery',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'TRV-Name Battery',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.trv_name_battery',
@@ -145,7 +145,7 @@
'supported_features': 0,
'translation_key': 'valve_position',
'unique_id': '123456789ABC-blutrv:200-valve_position',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_blu_trv_sensor_entity[sensor.trv_name_valve_position-state]
@@ -153,7 +153,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'TRV-Name Valve position',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.trv_name_valve_position',
@@ -199,7 +199,7 @@
'supported_features': 0,
'translation_key': 'left_slot_level',
'unique_id': '123456789ABC-cury:0-cury_left_level',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_cury_sensor_entity[sensor.test_name_left_slot_level-state]
@@ -207,7 +207,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test name Left slot level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_name_left_slot_level',
@@ -303,7 +303,7 @@
'supported_features': 0,
'translation_key': 'right_slot_level',
'unique_id': '123456789ABC-cury:0-cury_right_level',
'unit_of_measurement': '%',
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
})
# ---
# name: test_cury_sensor_entity[sensor.test_name_right_slot_level-state]
@@ -311,7 +311,7 @@
'attributes': ReadOnlyDict({
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Test name Right slot level',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: '%',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_name_right_slot_level',
@@ -6,10 +6,12 @@ from unittest.mock import AsyncMock, create_autospec, patch
from aiohttp import ClientSession
from aiovodafone.api import VodafoneStationDevice
from aiovodafone.exceptions import VodafoneError
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.vodafone_station.const import DOMAIN, SCAN_INTERVAL
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
@@ -69,37 +71,45 @@ async def test_coordinator_device_cleanup(
assert f"Removing device: {DEVICE_1_HOST}" in caplog.text
async def test_coordinator_json_decode_error(
@pytest.mark.parametrize(
("error", "expected_init_calls", "expected_session_calls"),
[
(VodafoneError("Generic error"), 1, 1),
(
JSONDecodeError("Invalid JSON", "<html>stale session</html>", 0),
2,
2,
),
],
)
async def test_coordinator_exceptions(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_vodafone_station_router: AsyncMock,
mock_config_entry: MockConfigEntry,
error: Exception,
expected_init_calls: int,
expected_session_calls: int,
) -> None:
"""Test stale-session JSON response reinitializes API session."""
await setup_integration(hass, mock_config_entry)
mock_vodafone_station_router.get_devices_data.side_effect = JSONDecodeError(
"Invalid JSON",
"<html>stale session</html>",
0,
)
"""Test exception handling during update: setup retry, plus session reinit for stale sessions."""
mock_vodafone_station_router.get_devices_data.side_effect = error
new_session = create_autospec(ClientSession, instance=True)
with (
patch(
"homeassistant.components.vodafone_station.coordinator.init_device_class",
return_value=mock_vodafone_station_router,
) as mock_init_device_class,
patch(
"homeassistant.components.vodafone_station.coordinator.async_client_session",
AsyncMock(return_value=new_session),
) as mock_async_client_session,
):
mock_init_device_class.return_value = mock_vodafone_station_router
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
mock_async_client_session.assert_awaited_once_with(hass)
mock_init_device_class.assert_called_once()
assert mock_init_device_class.call_args.args[2] == mock_config_entry.data
assert mock_init_device_class.call_args.args[3] == new_session
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
assert mock_init_device_class.call_count == expected_init_calls
assert mock_async_client_session.await_count == expected_session_calls
assert mock_init_device_class.call_args.args[2] == mock_config_entry.data
assert mock_init_device_class.call_args.args[3] == new_session