Compare commits

...

18 Commits

Author SHA1 Message Date
J. Nick Koston
7f4063f91e Bump aiodns to 4.0.0 (#160707) 2026-01-11 07:31:31 -10:00
mattreim
080ba46885 Add model id RODRET wireless dimmer (#160636) 2026-01-11 18:22:19 +01:00
Brett Adams
2cb028ee79 Catch any migration failures in Teslemetry (#160549) 2026-01-11 16:46:30 +01:00
mettolen
72655dbf0b Pump pysaunum to 0.2.0 (#160668) 2026-01-11 16:14:45 +01:00
Erwin Douna
153278221d Bump pytado 0.18.16 (#160724) 2026-01-11 13:24:22 +01:00
Daniel Hjelseth Høyer
4942ce7e86 Better handling of ratelimiting from Tibber (#160599)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-11 11:40:27 +01:00
hanwg
98e918cd8a Improve polling error messages for Telegram bot (#160675) 2026-01-11 06:54:50 +01:00
J. Nick Koston
1efc87bfef Bump easyenergy to 2.2.0 (#160709) 2026-01-10 18:54:50 -10:00
Simon Delberghe
b4360ccbd9 Move condition to prioritize preset mode (eco/comfort...) instead of program name in Overkiz (#160189) 2026-01-10 23:58:19 +01:00
Ernst Klamer
ce234d69a7 Revert bthome-ble back to 3.16.0 to fix missing data (#160694) 2026-01-10 09:47:30 -10:00
Álvaro Fernández Rojas
b2a198e230 Update aioairzone to v1.0.5 (#160688) 2026-01-10 20:43:10 +01:00
Michael Hansen
538009d2df Bump pysilero-vad to 3.2.0 (#160691) 2026-01-10 13:35:46 -06:00
Clifford Roche
99329851a2 Bump greeclimate to 2.1.1 (#160683) 2026-01-10 19:51:04 +01:00
DeerMaximum
f8ec395e96 Use snapshots for binary sensor tests in Nina (#160532) 2026-01-10 17:47:29 +01:00
mettolen
98fe189edf Add recalibrate CO2 button to Airobot (#160679) 2026-01-10 17:37:14 +01:00
Samuel Xiao
7b413e3fd3 Bumb switchbot api to v2.10.0 (#160657) 2026-01-10 13:01:55 +01:00
Paul Tarjan
00ca5473d4 Bump pyhik to 0.4.0 (#160654) 2026-01-10 08:04:29 +01:00
Martin Hjelmare
33c808713e Fix Z-Wave creating notification binary sensor for idle state (#160604) 2026-01-10 02:43:13 +01:00
39 changed files with 565 additions and 414 deletions

View File

@@ -43,6 +43,13 @@ BUTTON_TYPES: tuple[AirobotButtonEntityDescription, ...] = (
entity_category=EntityCategory.CONFIG,
press_fn=lambda coordinator: coordinator.client.reboot_thermostat(),
),
AirobotButtonEntityDescription(
key="recalibrate_co2",
translation_key="recalibrate_co2",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
press_fn=lambda coordinator: coordinator.client.recalibrate_co2_sensor(),
),
)

View File

@@ -1,5 +1,10 @@
{
"entity": {
"button": {
"recalibrate_co2": {
"default": "mdi:molecule-co2"
}
},
"number": {
"hysteresis_band": {
"default": "mdi:delta"

View File

@@ -59,6 +59,11 @@
}
},
"entity": {
"button": {
"recalibrate_co2": {
"name": "Recalibrate CO2 sensor"
}
},
"number": {
"hysteresis_band": {
"name": "Hysteresis band"

View File

@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==1.0.4"]
"requirements": ["aioairzone==1.0.5"]
}

View File

@@ -8,5 +8,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["pysilero-vad==3.1.0", "pyspeex-noise==1.0.2"]
"requirements": ["pysilero-vad==3.2.0", "pyspeex-noise==1.0.2"]
}

View File

@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==3.17.0"]
"requirements": ["bthome-ble==3.16.0"]
}

View File

@@ -169,6 +169,7 @@ FRIENDS_OF_HUE_SWITCH = {
}
RODRET_REMOTE_MODEL = "RODRET Dimmer"
RODRET_REMOTE_MODEL_2 = "RODRET wireless dimmer"
RODRET_REMOTE = {
(CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002},
(CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001},
@@ -624,6 +625,7 @@ REMOTES = {
HUE_WALL_REMOTE_MODEL: HUE_WALL_REMOTE,
FRIENDS_OF_HUE_SWITCH_MODEL: FRIENDS_OF_HUE_SWITCH,
RODRET_REMOTE_MODEL: RODRET_REMOTE,
RODRET_REMOTE_MODEL_2: RODRET_REMOTE,
SOMRIG_REMOTE_MODEL: SOMRIG_REMOTE,
STYRBAR_REMOTE_MODEL: STYRBAR_REMOTE,
SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER,

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["aiodns==3.6.1"]
"requirements": ["aiodns==4.0.0"]
}

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/easyenergy",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["easyenergy==2.1.2"],
"requirements": ["easyenergy==2.2.0"],
"single_config_entry": true
}

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/gree",
"iot_class": "local_polling",
"loggers": ["greeclimate"],
"requirements": ["greeclimate==2.1.0"]
"requirements": ["greeclimate==2.1.1"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["pyhik"],
"quality_scale": "legacy",
"requirements": ["pyHik==0.3.4"]
"requirements": ["pyHik==0.4.0"]
}

View File

@@ -50,7 +50,6 @@ rules:
Use load_json_object_fixture in tests
Patch the library instead of the HTTP requests
Create a shared fixture for the mock config entry
Use snapshots for binary sensor tests
Use init_integration in tests
Evaluate the need of test_config_entry_not_ready

View File

@@ -128,15 +128,15 @@ class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint(
states = self.device.states
if (
operating_mode := states[OverkizState.CORE_OPERATING_MODE]
) and operating_mode.value_as_str == OverkizCommandParam.EXTERNAL:
return PRESET_EXTERNAL
if (
state := states[OverkizState.IO_TARGET_HEATING_LEVEL]
) and state.value_as_str:
return OVERKIZ_TO_PRESET_MODE[state.value_as_str]
if (
operating_mode := states[OverkizState.CORE_OPERATING_MODE]
) and operating_mode.value_as_str == OverkizCommandParam.EXTERNAL:
return PRESET_EXTERNAL
return None
async def async_set_preset_mode(self, preset_mode: str) -> None:

View File

@@ -26,11 +26,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: LeilSaunaConfigEntry) ->
"""Set up Saunum Leil Sauna from a config entry."""
host = entry.data[CONF_HOST]
client = SaunumClient(host=host)
# Test connection
try:
await client.connect()
client = await SaunumClient.create(host)
except SaunumConnectionError as exc:
raise ConfigEntryNotReady(f"Error connecting to {host}: {exc}") from exc

View File

@@ -30,10 +30,9 @@ async def validate_input(data: dict[str, Any]) -> None:
"""
host = data[CONF_HOST]
client = SaunumClient(host=host)
client = await SaunumClient.create(host)
try:
await client.connect()
# Try to read data to verify communication
await client.async_get_data()
finally:

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["pysaunum"],
"quality_scale": "gold",
"requirements": ["pysaunum==0.1.0"]
"requirements": ["pysaunum==0.2.0"]
}

View File

@@ -73,7 +73,7 @@ rules:
comment: Integration controls a single device; no dynamic device discovery needed.
# Platinum
async-dependency: todo
async-dependency: done
inject-websession:
status: exempt
comment: Integration uses Modbus TCP protocol and does not make HTTP requests.

View File

@@ -13,5 +13,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["switchbot_api"],
"requirements": ["switchbot-api==2.9.0"]
"requirements": ["switchbot-api==2.10.0"]
}

View File

@@ -15,5 +15,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["PyTado"],
"requirements": ["python-tado==0.18.15"]
"requirements": ["python-tado==0.18.16"]
}

View File

@@ -24,13 +24,13 @@ async def async_setup_platform(
return pollbot
async def process_error(update: object, context: CallbackContext) -> None:
async def process_error(bot: Bot, update: object, context: CallbackContext) -> None:
"""Telegram bot error handler."""
if context.error:
error_callback(context.error, update)
error_callback(bot, context.error, update)
def error_callback(error: Exception, update: object | None = None) -> None:
def error_callback(bot: Bot, error: Exception, update: object | None = None) -> None:
"""Log the error."""
try:
raise error
@@ -39,9 +39,17 @@ def error_callback(error: Exception, update: object | None = None) -> None:
pass
except TelegramError:
if update is not None:
_LOGGER.error('Update "%s" caused error: "%s"', update, error)
_LOGGER.error(
'[%s %s] Update "%s" caused error: "%s"',
bot.username,
bot.id,
update,
error,
)
else:
_LOGGER.error("%s: %s", error.__class__.__name__, error)
_LOGGER.error(
"[%s %s] %s: %s", bot.username, bot.id, error.__class__.__name__, error
)
class PollBot(BaseTelegramBot):
@@ -58,7 +66,9 @@ class PollBot(BaseTelegramBot):
self.bot = bot
self.application = ApplicationBuilder().bot(self.bot).build()
self.application.add_handler(TypeHandler(Update, self.handle_update))
self.application.add_error_handler(process_error)
self.application.add_error_handler(
lambda update, context: process_error(self.bot, update, context)
)
async def shutdown(self) -> None:
"""Shutdown the app."""
@@ -66,16 +76,18 @@ class PollBot(BaseTelegramBot):
async def start_polling(self) -> None:
"""Start the polling task."""
_LOGGER.debug("Starting polling")
await self.application.initialize()
if self.application.updater:
await self.application.updater.start_polling(error_callback=error_callback)
await self.application.updater.start_polling(
error_callback=lambda error: error_callback(self.bot, error, None)
)
await self.application.start()
_LOGGER.info("[%s %s] Started polling", self.bot.username, self.bot.id)
async def stop_polling(self) -> None:
"""Stop the polling task."""
_LOGGER.debug("Stopping polling")
if self.application.updater:
await self.application.updater.stop()
await self.application.stop()
await self.application.shutdown()
_LOGGER.info("[%s %s] Stopped polling", self.bot.username, self.bot.id)

View File

@@ -5,6 +5,7 @@ from collections.abc import Callable
from typing import Final
from aiohttp import ClientResponseError
from aiohttp.client_exceptions import ClientError
from tesla_fleet_api.const import Scope
from tesla_fleet_api.exceptions import (
Forbidden,
@@ -315,7 +316,7 @@ async def async_migrate_entry(
data = await Teslemetry(session, access_token).migrate_to_oauth(
CLIENT_ID, access_token, hass.config.location_name
)
except ClientResponseError as e:
except (ClientError, TypeError) as e:
raise ConfigEntryAuthFailed from e
# Add auth_implementation for OAuth2 flow compatibility

View File

@@ -250,6 +250,12 @@ class TibberDataAPICoordinator(DataUpdateCoordinator[dict[str, TibberDevice]]):
async def _async_update_data(self) -> dict[str, TibberDevice]:
"""Fetch the latest device capabilities from the Tibber Data API."""
client = await self._async_get_client()
devices: dict[str, TibberDevice] = await client.update_devices()
try:
devices: dict[str, TibberDevice] = await client.update_devices()
except tibber.exceptions.RateLimitExceededError as err:
raise UpdateFailed(
f"Rate limit exceeded, retry after {err.retry_after} seconds",
retry_after=err.retry_after,
) from err
self._build_sensor_lookup(devices)
return devices

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/tibber",
"iot_class": "cloud_polling",
"loggers": ["tibber"],
"requirements": ["pyTibber==0.34.1"]
"requirements": ["pyTibber==0.34.4"]
}

View File

@@ -654,6 +654,7 @@ DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
key=NOTIFICATION_SMOKE_ALARM,
entity_category=EntityCategory.DIAGNOSTIC,
not_states={
0,
SmokeAlarmNotificationEvent.SENSOR_STATUS_SMOKE_DETECTED_LOCATION_PROVIDED,
SmokeAlarmNotificationEvent.SENSOR_STATUS_SMOKE_DETECTED,
SmokeAlarmNotificationEvent.MAINTENANCE_STATUS_REPLACEMENT_REQUIRED,

View File

@@ -2,7 +2,7 @@
aiodhcpwatcher==1.2.1
aiodiscover==2.7.1
aiodns==3.6.1
aiodns==4.0.0
aiohasupervisor==0.3.3
aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.3.0
@@ -56,7 +56,7 @@ PyJWT==2.10.1
PyNaCl==1.6.0
pyOpenSSL==25.3.0
pyserial==3.5
pysilero-vad==3.1.0
pysilero-vad==3.2.0
pyspeex-noise==1.0.2
python-slugify==8.0.4
PyTurboJPEG==1.8.0

View File

@@ -24,7 +24,7 @@ classifiers = [
]
requires-python = ">=3.13.2"
dependencies = [
"aiodns==3.6.1",
"aiodns==4.0.0",
# Integrations may depend on hassio integration without listing it to
# change behavior based on presence of supervisor. Deprecated with #127228
# Lib can be removed with 2025.11

4
requirements.txt generated
View File

@@ -3,7 +3,7 @@
-c homeassistant/package_constraints.txt
# Home Assistant Core
aiodns==3.6.1
aiodns==4.0.0
aiohasupervisor==0.3.3
aiohttp-asyncmdnsresolver==0.1.1
aiohttp-fast-zlib==0.3.0
@@ -40,7 +40,7 @@ propcache==0.4.1
psutil-home-assistant==0.0.1
PyJWT==2.10.1
pyOpenSSL==25.3.0
pysilero-vad==3.1.0
pysilero-vad==3.2.0
pyspeex-noise==1.0.2
python-slugify==8.0.4
PyTurboJPEG==1.8.0

22
requirements_all.txt generated
View File

@@ -187,7 +187,7 @@ aioairq==0.4.7
aioairzone-cloud==0.7.2
# homeassistant.components.airzone
aioairzone==1.0.4
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==11.0.2
@@ -231,7 +231,7 @@ aiodhcpwatcher==1.2.1
aiodiscover==2.7.1
# homeassistant.components.dnsip
aiodns==3.6.1
aiodns==4.0.0
# homeassistant.components.duke_energy
aiodukeenergy==0.3.0
@@ -703,7 +703,7 @@ brottsplatskartan==1.0.5
brunt==1.2.0
# homeassistant.components.bthome
bthome-ble==3.17.0
bthome-ble==3.16.0
# homeassistant.components.bt_home_hub_5
bthomehub5-devicelist==0.1.1
@@ -842,7 +842,7 @@ dynalite-panel==0.0.4
eagle100==0.1.1
# homeassistant.components.easyenergy
easyenergy==2.1.2
easyenergy==2.2.0
# homeassistant.components.ebusd
ebusdpy==0.0.17
@@ -1127,7 +1127,7 @@ gpiozero==1.6.2
gps3==0.33.3
# homeassistant.components.gree
greeclimate==2.1.0
greeclimate==2.1.1
# homeassistant.components.greeneye_monitor
greeneye_monitor==3.0.3
@@ -1855,7 +1855,7 @@ pyElectra==1.2.4
pyEmby==1.10
# homeassistant.components.hikvision
pyHik==0.3.4
pyHik==0.4.0
# homeassistant.components.homee
pyHomee==1.3.8
@@ -1867,7 +1867,7 @@ pyRFXtrx==0.31.1
pySDCP==1
# homeassistant.components.tibber
pyTibber==0.34.1
pyTibber==0.34.4
# homeassistant.components.dlink
pyW215==0.8.0
@@ -2375,7 +2375,7 @@ pysabnzbd==1.1.1
pysaj==0.0.16
# homeassistant.components.saunum
pysaunum==0.1.0
pysaunum==0.2.0
# homeassistant.components.schlage
pyschlage==2025.9.0
@@ -2409,7 +2409,7 @@ pysiaalarm==3.1.1
pysignalclirestapi==0.3.24
# homeassistant.components.assist_pipeline
pysilero-vad==3.1.0
pysilero-vad==3.2.0
# homeassistant.components.sky_hub
pyskyqhub==0.1.4
@@ -2596,7 +2596,7 @@ python-snoo==0.8.3
python-songpal==0.16.2
# homeassistant.components.tado
python-tado==0.18.15
python-tado==0.18.16
# homeassistant.components.technove
python-technove==2.0.0
@@ -2956,7 +2956,7 @@ surepy==0.9.0
swisshydrodata==0.1.0
# homeassistant.components.switchbot_cloud
switchbot-api==2.9.0
switchbot-api==2.10.0
# homeassistant.components.synology_srm
synology-srm==0.2.0

View File

@@ -178,7 +178,7 @@ aioairq==0.4.7
aioairzone-cloud==0.7.2
# homeassistant.components.airzone
aioairzone==1.0.4
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==11.0.2
@@ -222,7 +222,7 @@ aiodhcpwatcher==1.2.1
aiodiscover==2.7.1
# homeassistant.components.dnsip
aiodns==3.6.1
aiodns==4.0.0
# homeassistant.components.duke_energy
aiodukeenergy==0.3.0
@@ -633,7 +633,7 @@ brottsplatskartan==1.0.5
brunt==1.2.0
# homeassistant.components.bthome
bthome-ble==3.17.0
bthome-ble==3.16.0
# homeassistant.components.buienradar
buienradar==1.0.6
@@ -748,7 +748,7 @@ dynalite-panel==0.0.4
eagle100==0.1.1
# homeassistant.components.easyenergy
easyenergy==2.1.2
easyenergy==2.2.0
# homeassistant.components.egauge
egauge-async==0.4.0
@@ -1000,7 +1000,7 @@ govee-local-api==2.3.0
gps3==0.33.3
# homeassistant.components.gree
greeclimate==2.1.0
greeclimate==2.1.1
# homeassistant.components.greeneye_monitor
greeneye_monitor==3.0.3
@@ -1589,7 +1589,7 @@ pyDuotecno==2024.10.1
pyElectra==1.2.4
# homeassistant.components.hikvision
pyHik==0.3.4
pyHik==0.4.0
# homeassistant.components.homee
pyHomee==1.3.8
@@ -1598,7 +1598,7 @@ pyHomee==1.3.8
pyRFXtrx==0.31.1
# homeassistant.components.tibber
pyTibber==0.34.1
pyTibber==0.34.4
# homeassistant.components.dlink
pyW215==0.8.0
@@ -2007,7 +2007,7 @@ pyrympro==0.0.9
pysabnzbd==1.1.1
# homeassistant.components.saunum
pysaunum==0.1.0
pysaunum==0.2.0
# homeassistant.components.schlage
pyschlage==2025.9.0
@@ -2035,7 +2035,7 @@ pysiaalarm==3.1.1
pysignalclirestapi==0.3.24
# homeassistant.components.assist_pipeline
pysilero-vad==3.1.0
pysilero-vad==3.2.0
# homeassistant.components.sma
pysma==1.1.0
@@ -2183,7 +2183,7 @@ python-snoo==0.8.3
python-songpal==0.16.2
# homeassistant.components.tado
python-tado==0.18.15
python-tado==0.18.16
# homeassistant.components.technove
python-technove==2.0.0
@@ -2477,7 +2477,7 @@ subarulink==0.7.15
surepy==0.9.0
# homeassistant.components.switchbot_cloud
switchbot-api==2.9.0
switchbot-api==2.10.0
# homeassistant.components.system_bridge
systembridgeconnector==5.3.1

View File

@@ -1,4 +1,52 @@
# serializer version: 1
# name: test_buttons[button.test_thermostat_recalibrate_co2_sensor-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.test_thermostat_recalibrate_co2_sensor',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Recalibrate CO2 sensor',
'platform': 'airobot',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'recalibrate_co2',
'unique_id': 'T01A1B2C3_recalibrate_co2',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[button.test_thermostat_recalibrate_co2_sensor-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Test Thermostat Recalibrate CO2 sensor',
}),
'context': <ANY>,
'entity_id': 'button.test_thermostat_recalibrate_co2_sensor',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[button.test_thermostat_restart-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -25,7 +25,7 @@ def platforms() -> list[Platform]:
return [Platform.BUTTON]
@pytest.mark.usefixtures("init_integration")
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_buttons(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
@@ -93,3 +93,38 @@ async def test_restart_button_connection_errors(
)
mock_airobot_client.reboot_thermostat.assert_called_once()
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_recalibrate_co2_button(
hass: HomeAssistant,
mock_airobot_client: AsyncMock,
) -> None:
"""Test recalibrate CO2 sensor button."""
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: "button.test_thermostat_recalibrate_co2_sensor"},
blocking=True,
)
mock_airobot_client.recalibrate_co2_sensor.assert_called_once()
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
async def test_recalibrate_co2_button_error(
hass: HomeAssistant,
mock_airobot_client: AsyncMock,
) -> None:
"""Test recalibrate CO2 sensor button error handling."""
mock_airobot_client.recalibrate_co2_sensor.side_effect = AirobotError("Test error")
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: "button.test_thermostat_recalibrate_co2_sensor"},
blocking=True,
)
mock_airobot_client.recalibrate_co2_sensor.assert_called_once()

View File

@@ -2,8 +2,24 @@
import json
from typing import Any
from unittest.mock import patch
from tests.common import load_fixture
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture
async def setup_platform(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Set up the NINA platform."""
with patch(
"pynina.baseApi.BaseAPI._makeRequest",
wraps=mocked_request_function,
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
def mocked_request_function(url: str) -> dict[str, Any]:

View File

@@ -0,0 +1,257 @@
# serializer version: 1
# name: test_sensors[binary_sensor.nina_warning_aach_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.nina_warning_aach_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.SAFETY: 'safety'>,
'original_icon': None,
'original_name': 'Warning: Aach 1',
'platform': 'nina',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '095760000000-1',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[binary_sensor.nina_warning_aach_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'affected_areas': 'Gemeinde Oberreichenbach, Gemeinde Neuweiler, Stadt Nagold, Stadt Neubulach, Gemeinde Schömberg, Gemeinde Simmersfeld, Gemeinde Simmozheim, Gemeinde Rohrdorf, Gemeinde Ostelsheim, Gemeinde Ebhausen, Gemeinde Egenhausen, Gemeinde Dobel, Stadt Bad Liebenzell, Stadt Solingen, Stadt Haiterbach, Stadt Bad Herrenalb, Gemeinde Höfen an der Enz, Gemeinde Gechingen, Gemeinde Enzklösterle, Gemeinde Gutach (Schwarzwaldbahn) und 3392 weitere.',
'description': 'Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden.',
'device_class': 'safety',
'expires': '3021-11-22T05:19:00+01:00',
'friendly_name': 'NINA Warning: Aach 1',
'headline': 'Ausfall Notruf 112',
'id': 'mow.DE-NW-BN-SE030-20201014-30-000',
'recommended_actions': '',
'sender': 'Deutscher Wetterdienst',
'sent': '2021-10-11T05:20:00+01:00',
'severity': 'Minor',
'start': '2021-11-01T05:20:00+01:00',
'web': 'https://www.wettergefahren.de',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.nina_warning_aach_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_sensors[binary_sensor.nina_warning_aach_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.nina_warning_aach_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.SAFETY: 'safety'>,
'original_icon': None,
'original_name': 'Warning: Aach 2',
'platform': 'nina',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '095760000000-2',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[binary_sensor.nina_warning_aach_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'safety',
'friendly_name': 'NINA Warning: Aach 2',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.nina_warning_aach_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_sensors[binary_sensor.nina_warning_aach_3-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.nina_warning_aach_3',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.SAFETY: 'safety'>,
'original_icon': None,
'original_name': 'Warning: Aach 3',
'platform': 'nina',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '095760000000-3',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[binary_sensor.nina_warning_aach_3-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'safety',
'friendly_name': 'NINA Warning: Aach 3',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.nina_warning_aach_3',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_sensors[binary_sensor.nina_warning_aach_4-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.nina_warning_aach_4',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.SAFETY: 'safety'>,
'original_icon': None,
'original_name': 'Warning: Aach 4',
'platform': 'nina',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '095760000000-4',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[binary_sensor.nina_warning_aach_4-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'safety',
'friendly_name': 'NINA Warning: Aach 4',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.nina_warning_aach_4',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_sensors[binary_sensor.nina_warning_aach_5-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.nina_warning_aach_5',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.SAFETY: 'safety'>,
'original_icon': None,
'original_name': 'Warning: Aach 5',
'platform': 'nina',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '095760000000-5',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[binary_sensor.nina_warning_aach_5-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'safety',
'friendly_name': 'NINA Warning: Aach 5',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.nina_warning_aach_5',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@@ -3,40 +3,17 @@
from __future__ import annotations
from typing import Any
from unittest.mock import patch
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.nina.const import (
ATTR_AFFECTED_AREAS,
ATTR_DESCRIPTION,
ATTR_EXPIRES,
ATTR_HEADLINE,
ATTR_ID,
ATTR_RECOMMENDED_ACTIONS,
ATTR_SENDER,
ATTR_SENT,
ATTR_SEVERITY,
ATTR_START,
ATTR_WEB,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.nina.const import ATTR_HEADLINE, DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import mocked_request_function
from . import setup_platform
from tests.common import MockConfigEntry
ENTRY_DATA: dict[str, Any] = {
"slots": 5,
"regions": {"083350000000": "Aach, Stadt"},
"filters": {
"headline_filter": ".*corona.*",
"area_filter": ".*",
},
}
from tests.common import MockConfigEntry, snapshot_platform
ENTRY_DATA_NO_CORONA: dict[str, Any] = {
"slots": 5,
@@ -47,7 +24,7 @@ ENTRY_DATA_NO_CORONA: dict[str, Any] = {
},
}
ENTRY_DATA_NO_AREA: dict[str, Any] = {
ENTRY_DATA_SPECIFIC_AREA: dict[str, Any] = {
"slots": 5,
"regions": {"083350000000": "Aach, Stadt"},
"filters": {
@@ -57,321 +34,93 @@ ENTRY_DATA_NO_AREA: dict[str, Any] = {
}
async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) -> None:
async def test_sensors(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the creation and values of the NINA sensors."""
with patch(
"pynina.baseApi.BaseAPI._makeRequest",
wraps=mocked_request_function,
):
conf_entry: MockConfigEntry = MockConfigEntry(
domain=DOMAIN, title="NINA", data=ENTRY_DATA, version=1, minor_version=3
)
conf_entry.add_to_hass(hass)
await hass.config_entries.async_setup(conf_entry.entry_id)
await hass.async_block_till_done()
assert conf_entry.state is ConfigEntryState.LOADED
state_w1 = hass.states.get("binary_sensor.nina_warning_aach_stadt_1")
entry_w1 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_1")
assert state_w1.state == STATE_ON
assert state_w1.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112"
assert (
state_w1.attributes.get(ATTR_DESCRIPTION)
== "Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden."
)
assert state_w1.attributes.get(ATTR_SENDER) == "Deutscher Wetterdienst"
assert state_w1.attributes.get(ATTR_SEVERITY) == "Minor"
assert state_w1.attributes.get(ATTR_RECOMMENDED_ACTIONS) == ""
assert state_w1.attributes.get(ATTR_WEB) == "https://www.wettergefahren.de"
assert (
state_w1.attributes.get(ATTR_AFFECTED_AREAS)
== "Gemeinde Oberreichenbach, Gemeinde Neuweiler, Stadt Nagold, Stadt Neubulach, Gemeinde Schömberg, Gemeinde Simmersfeld, Gemeinde Simmozheim, Gemeinde Rohrdorf, Gemeinde Ostelsheim, Gemeinde Ebhausen, Gemeinde Egenhausen, Gemeinde Dobel, Stadt Bad Liebenzell, Stadt Solingen, Stadt Haiterbach, Stadt Bad Herrenalb, Gemeinde Höfen an der Enz, Gemeinde Gechingen, Gemeinde Enzklösterle, Gemeinde Gutach (Schwarzwaldbahn) und 3392 weitere."
)
assert state_w1.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000"
assert state_w1.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00"
assert state_w1.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00"
assert state_w1.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00"
assert entry_w1.unique_id == "083350000000-1"
assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
state_w2 = hass.states.get("binary_sensor.nina_warning_aach_stadt_2")
entry_w2 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_2")
assert state_w2.state == STATE_OFF
assert state_w2.attributes.get(ATTR_HEADLINE) is None
assert state_w2.attributes.get(ATTR_DESCRIPTION) is None
assert state_w2.attributes.get(ATTR_SENDER) is None
assert state_w2.attributes.get(ATTR_SEVERITY) is None
assert state_w2.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None
assert state_w2.attributes.get(ATTR_WEB) is None
assert state_w2.attributes.get(ATTR_AFFECTED_AREAS) is None
assert state_w2.attributes.get(ATTR_ID) is None
assert state_w2.attributes.get(ATTR_SENT) is None
assert state_w2.attributes.get(ATTR_START) is None
assert state_w2.attributes.get(ATTR_EXPIRES) is None
assert entry_w2.unique_id == "083350000000-2"
assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
state_w3 = hass.states.get("binary_sensor.nina_warning_aach_stadt_3")
entry_w3 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_3")
assert state_w3.state == STATE_OFF
assert state_w3.attributes.get(ATTR_HEADLINE) is None
assert state_w3.attributes.get(ATTR_DESCRIPTION) is None
assert state_w3.attributes.get(ATTR_SENDER) is None
assert state_w3.attributes.get(ATTR_SEVERITY) is None
assert state_w3.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None
assert state_w3.attributes.get(ATTR_WEB) is None
assert state_w3.attributes.get(ATTR_AFFECTED_AREAS) is None
assert state_w3.attributes.get(ATTR_ID) is None
assert state_w3.attributes.get(ATTR_SENT) is None
assert state_w3.attributes.get(ATTR_START) is None
assert state_w3.attributes.get(ATTR_EXPIRES) is None
assert entry_w3.unique_id == "083350000000-3"
assert state_w3.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
state_w4 = hass.states.get("binary_sensor.nina_warning_aach_stadt_4")
entry_w4 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_4")
assert state_w4.state == STATE_OFF
assert state_w4.attributes.get(ATTR_HEADLINE) is None
assert state_w4.attributes.get(ATTR_DESCRIPTION) is None
assert state_w4.attributes.get(ATTR_SENDER) is None
assert state_w4.attributes.get(ATTR_SEVERITY) is None
assert state_w4.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None
assert state_w4.attributes.get(ATTR_WEB) is None
assert state_w4.attributes.get(ATTR_AFFECTED_AREAS) is None
assert state_w4.attributes.get(ATTR_ID) is None
assert state_w4.attributes.get(ATTR_SENT) is None
assert state_w4.attributes.get(ATTR_START) is None
assert state_w4.attributes.get(ATTR_EXPIRES) is None
assert entry_w4.unique_id == "083350000000-4"
assert state_w4.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
state_w5 = hass.states.get("binary_sensor.nina_warning_aach_stadt_5")
entry_w5 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_5")
assert state_w5.state == STATE_OFF
assert state_w5.attributes.get(ATTR_HEADLINE) is None
assert state_w5.attributes.get(ATTR_DESCRIPTION) is None
assert state_w5.attributes.get(ATTR_SENDER) is None
assert state_w5.attributes.get(ATTR_SEVERITY) is None
assert state_w5.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None
assert state_w5.attributes.get(ATTR_WEB) is None
assert state_w5.attributes.get(ATTR_AFFECTED_AREAS) is None
assert state_w5.attributes.get(ATTR_ID) is None
assert state_w5.attributes.get(ATTR_SENT) is None
assert state_w5.attributes.get(ATTR_START) is None
assert state_w5.attributes.get(ATTR_EXPIRES) is None
assert entry_w5.unique_id == "083350000000-5"
assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
await setup_platform(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_sensors_without_corona_filter(
hass: HomeAssistant, entity_registry: er.EntityRegistry
hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion
) -> None:
"""Test the creation and values of the NINA sensors without the corona filter."""
with patch(
"pynina.baseApi.BaseAPI._makeRequest",
wraps=mocked_request_function,
):
conf_entry: MockConfigEntry = MockConfigEntry(
domain=DOMAIN,
title="NINA",
data=ENTRY_DATA_NO_CORONA,
version=1,
minor_version=3,
)
conf_entry.add_to_hass(hass)
conf_entry: MockConfigEntry = MockConfigEntry(
domain=DOMAIN,
title="NINA",
data=ENTRY_DATA_NO_CORONA,
version=1,
minor_version=3,
)
conf_entry.add_to_hass(hass)
await hass.config_entries.async_setup(conf_entry.entry_id)
await hass.async_block_till_done()
await setup_platform(hass, conf_entry)
assert conf_entry.state is ConfigEntryState.LOADED
state_w1 = hass.states.get("binary_sensor.nina_warning_aach_stadt_1")
state_w1 = hass.states.get("binary_sensor.nina_warning_aach_stadt_1")
entry_w1 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_1")
assert state_w1.state == STATE_ON
assert (
state_w1.attributes.get(ATTR_HEADLINE)
== "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen"
)
assert state_w1.state == STATE_ON
assert (
state_w1.attributes.get(ATTR_HEADLINE)
== "Corona-Verordnung des Landes: Warnstufe durch Landesgesundheitsamt ausgerufen"
)
assert (
state_w1.attributes.get(ATTR_DESCRIPTION)
== "Die Zahl der mit dem Corona-Virus infizierten Menschen steigt gegenwärtig stark an. Es wächst daher die Gefahr einer weiteren Verbreitung der Infektion und - je nach Einzelfall - auch von schweren Erkrankungen."
)
assert state_w1.attributes.get(ATTR_SENDER) == ""
assert state_w1.attributes.get(ATTR_SEVERITY) == "Minor"
assert (
state_w1.attributes.get(ATTR_RECOMMENDED_ACTIONS)
== "Waschen sich regelmäßig und gründlich die Hände."
)
assert state_w1.attributes.get(ATTR_WEB) == ""
assert (
state_w1.attributes.get(ATTR_AFFECTED_AREAS)
== "Bundesland: Freie Hansestadt Bremen, Land Berlin, Land Hessen, Land Nordrhein-Westfalen, Land Brandenburg, Freistaat Bayern, Land Mecklenburg-Vorpommern, Land Rheinland-Pfalz, Freistaat Sachsen, Land Schleswig-Holstein, Freie und Hansestadt Hamburg, Freistaat Thüringen, Land Niedersachsen, Land Saarland, Land Sachsen-Anhalt, Land Baden-Württemberg"
)
assert state_w1.attributes.get(ATTR_ID) == "mow.DE-BW-S-SE018-20211102-18-001"
assert state_w1.attributes.get(ATTR_SENT) == "2021-11-02T20:07:16+01:00"
assert state_w1.attributes.get(ATTR_START) == ""
assert state_w1.attributes.get(ATTR_EXPIRES) == ""
state_w2 = hass.states.get("binary_sensor.nina_warning_aach_stadt_2")
assert entry_w1.unique_id == "083350000000-1"
assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
assert state_w2.state == STATE_ON
assert state_w2.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112"
state_w2 = hass.states.get("binary_sensor.nina_warning_aach_stadt_2")
entry_w2 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_2")
state_w3 = hass.states.get("binary_sensor.nina_warning_aach_stadt_3")
assert state_w2.state == STATE_ON
assert state_w2.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112"
assert (
state_w2.attributes.get(ATTR_DESCRIPTION)
== "Es treten Sturmböen mit Geschwindigkeiten zwischen 70 km/h (20m/s, 38kn, Bft 8) und 85 km/h (24m/s, 47kn, Bft 9) aus westlicher Richtung auf. In Schauernähe sowie in exponierten Lagen muss mit schweren Sturmböen bis 90 km/h (25m/s, 48kn, Bft 10) gerechnet werden."
)
assert (
state_w2.attributes.get(ATTR_AFFECTED_AREAS)
== "Gemeinde Oberreichenbach, Gemeinde Neuweiler, Stadt Nagold, Stadt Neubulach, Gemeinde Schömberg, Gemeinde Simmersfeld, Gemeinde Simmozheim, Gemeinde Rohrdorf, Gemeinde Ostelsheim, Gemeinde Ebhausen, Gemeinde Egenhausen, Gemeinde Dobel, Stadt Bad Liebenzell, Stadt Solingen, Stadt Haiterbach, Stadt Bad Herrenalb, Gemeinde Höfen an der Enz, Gemeinde Gechingen, Gemeinde Enzklösterle, Gemeinde Gutach (Schwarzwaldbahn) und 3392 weitere."
)
assert state_w2.attributes.get(ATTR_SENDER) == "Deutscher Wetterdienst"
assert state_w2.attributes.get(ATTR_SEVERITY) == "Minor"
assert state_w2.attributes.get(ATTR_RECOMMENDED_ACTIONS) == ""
assert state_w2.attributes.get(ATTR_WEB) == "https://www.wettergefahren.de"
assert state_w2.attributes.get(ATTR_ID) == "mow.DE-NW-BN-SE030-20201014-30-000"
assert state_w2.attributes.get(ATTR_SENT) == "2021-10-11T05:20:00+01:00"
assert state_w2.attributes.get(ATTR_START) == "2021-11-01T05:20:00+01:00"
assert state_w2.attributes.get(ATTR_EXPIRES) == "3021-11-22T05:19:00+01:00"
assert state_w3.state == STATE_OFF
assert entry_w2.unique_id == "083350000000-2"
assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
state_w4 = hass.states.get("binary_sensor.nina_warning_aach_stadt_4")
state_w3 = hass.states.get("binary_sensor.nina_warning_aach_stadt_3")
entry_w3 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_3")
assert state_w4.state == STATE_OFF
assert state_w3.state == STATE_OFF
assert state_w3.attributes.get(ATTR_HEADLINE) is None
assert state_w3.attributes.get(ATTR_DESCRIPTION) is None
assert state_w3.attributes.get(ATTR_SENDER) is None
assert state_w3.attributes.get(ATTR_SEVERITY) is None
assert state_w3.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None
assert state_w3.attributes.get(ATTR_WEB) is None
assert state_w3.attributes.get(ATTR_AFFECTED_AREAS) is None
assert state_w3.attributes.get(ATTR_ID) is None
assert state_w3.attributes.get(ATTR_SENT) is None
assert state_w3.attributes.get(ATTR_START) is None
assert state_w3.attributes.get(ATTR_EXPIRES) is None
state_w5 = hass.states.get("binary_sensor.nina_warning_aach_stadt_5")
assert entry_w3.unique_id == "083350000000-3"
assert state_w3.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
state_w4 = hass.states.get("binary_sensor.nina_warning_aach_stadt_4")
entry_w4 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_4")
assert state_w4.state == STATE_OFF
assert state_w4.attributes.get(ATTR_HEADLINE) is None
assert state_w4.attributes.get(ATTR_DESCRIPTION) is None
assert state_w4.attributes.get(ATTR_SENDER) is None
assert state_w4.attributes.get(ATTR_SEVERITY) is None
assert state_w4.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None
assert state_w4.attributes.get(ATTR_WEB) is None
assert state_w4.attributes.get(ATTR_AFFECTED_AREAS) is None
assert state_w4.attributes.get(ATTR_ID) is None
assert state_w4.attributes.get(ATTR_SENT) is None
assert state_w4.attributes.get(ATTR_START) is None
assert state_w4.attributes.get(ATTR_EXPIRES) is None
assert entry_w4.unique_id == "083350000000-4"
assert state_w4.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
state_w5 = hass.states.get("binary_sensor.nina_warning_aach_stadt_5")
entry_w5 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_5")
assert state_w5.state == STATE_OFF
assert state_w5.attributes.get(ATTR_HEADLINE) is None
assert state_w5.attributes.get(ATTR_DESCRIPTION) is None
assert state_w5.attributes.get(ATTR_SENDER) is None
assert state_w5.attributes.get(ATTR_SEVERITY) is None
assert state_w5.attributes.get(ATTR_RECOMMENDED_ACTIONS) is None
assert state_w5.attributes.get(ATTR_WEB) is None
assert state_w5.attributes.get(ATTR_AFFECTED_AREAS) is None
assert state_w5.attributes.get(ATTR_ID) is None
assert state_w5.attributes.get(ATTR_SENT) is None
assert state_w5.attributes.get(ATTR_START) is None
assert state_w5.attributes.get(ATTR_EXPIRES) is None
assert entry_w5.unique_id == "083350000000-5"
assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
assert state_w5.state == STATE_OFF
async def test_sensors_with_area_filter(
hass: HomeAssistant, entity_registry: er.EntityRegistry
hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion
) -> None:
"""Test the creation and values of the NINA sensors with an area filter."""
"""Test the creation and values of the NINA sensors with a restrictive area filter."""
with patch(
"pynina.baseApi.BaseAPI._makeRequest",
wraps=mocked_request_function,
):
conf_entry: MockConfigEntry = MockConfigEntry(
domain=DOMAIN,
title="NINA",
data=ENTRY_DATA_NO_AREA,
version=1,
minor_version=3,
)
conf_entry.add_to_hass(hass)
conf_entry: MockConfigEntry = MockConfigEntry(
domain=DOMAIN,
title="NINA",
data=ENTRY_DATA_SPECIFIC_AREA,
version=1,
minor_version=3,
)
conf_entry.add_to_hass(hass)
await hass.config_entries.async_setup(conf_entry.entry_id)
await hass.async_block_till_done()
await setup_platform(hass, conf_entry)
assert conf_entry.state is ConfigEntryState.LOADED
state_w1 = hass.states.get("binary_sensor.nina_warning_aach_stadt_1")
state_w1 = hass.states.get("binary_sensor.nina_warning_aach_stadt_1")
entry_w1 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_1")
assert state_w1.state == STATE_ON
assert state_w1.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112"
assert state_w1.state == STATE_ON
state_w2 = hass.states.get("binary_sensor.nina_warning_aach_stadt_2")
assert entry_w1.unique_id == "083350000000-1"
assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
assert state_w2.state == STATE_OFF
state_w2 = hass.states.get("binary_sensor.nina_warning_aach_stadt_2")
entry_w2 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_2")
state_w3 = hass.states.get("binary_sensor.nina_warning_aach_stadt_3")
assert state_w2.state == STATE_OFF
assert state_w3.state == STATE_OFF
assert entry_w2.unique_id == "083350000000-2"
assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
state_w4 = hass.states.get("binary_sensor.nina_warning_aach_stadt_4")
state_w3 = hass.states.get("binary_sensor.nina_warning_aach_stadt_3")
entry_w3 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_3")
assert state_w4.state == STATE_OFF
assert state_w3.state == STATE_OFF
state_w5 = hass.states.get("binary_sensor.nina_warning_aach_stadt_5")
assert entry_w3.unique_id == "083350000000-3"
assert state_w3.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
state_w4 = hass.states.get("binary_sensor.nina_warning_aach_stadt_4")
entry_w4 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_4")
assert state_w4.state == STATE_OFF
assert entry_w4.unique_id == "083350000000-4"
assert state_w4.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
state_w5 = hass.states.get("binary_sensor.nina_warning_aach_stadt_5")
entry_w5 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_5")
assert state_w5.state == STATE_OFF
assert entry_w5.unique_id == "083350000000-5"
assert state_w5.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY
assert state_w5.state == STATE_OFF

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from copy import deepcopy
import json
from typing import Any
from unittest.mock import AsyncMock, patch
@@ -28,14 +27,10 @@ from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import entity_registry as er
from . import mocked_request_function
from . import mocked_request_function, setup_platform
from .const import DUMMY_CONFIG_ENTRY, DUMMY_USER_INPUT
from tests.common import MockConfigEntry, load_fixture
DUMMY_RESPONSE_REGIONS: dict[str, Any] = json.loads(
load_fixture("sample_regions.json", "nina")
)
from tests.common import MockConfigEntry
def assert_dummy_entry_created(result: dict[str, Any]) -> None:
@@ -141,15 +136,15 @@ async def test_options_flow_init(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test config flow options."""
await setup_platform(hass, mock_config_entry)
with (
patch(
"pynina.baseApi.BaseAPI._makeRequest",
wraps=mocked_request_function,
),
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(
mock_config_entry.entry_id
)
@@ -195,15 +190,15 @@ async def test_options_flow_with_no_selection(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test config flow options with no selection."""
await setup_platform(hass, mock_config_entry)
with (
patch(
"pynina.baseApi.BaseAPI._makeRequest",
wraps=mocked_request_function,
),
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(
mock_config_entry.entry_id
)
@@ -264,13 +259,13 @@ async def test_options_flow_connection_error(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test config flow options but no connection."""
await setup_platform(hass, mock_config_entry)
with patch(
"pynina.baseApi.BaseAPI._makeRequest",
side_effect=ApiError("Could not connect to Api"),
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(
mock_config_entry.entry_id
)
@@ -283,15 +278,15 @@ async def test_options_flow_unexpected_exception(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test config flow options but with an unexpected exception."""
await setup_platform(hass, mock_config_entry)
with (
patch(
"pynina.baseApi.BaseAPI._makeRequest",
side_effect=Exception("DUMMY"),
),
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(
mock_config_entry.entry_id
)
@@ -313,15 +308,14 @@ async def test_options_flow_entity_removal(
)
config_entry.add_to_hass(hass)
await setup_platform(hass, config_entry)
with (
patch(
"pynina.baseApi.BaseAPI._makeRequest",
wraps=mocked_request_function,
),
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
result = await hass.config_entries.options.async_configure(

View File

@@ -2,7 +2,7 @@
from collections.abc import Generator
from datetime import timedelta
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
from pysaunum import SaunumData
import pytest
@@ -42,8 +42,8 @@ def mock_config_entry() -> MockConfigEntry:
@pytest.fixture
def mock_saunum_client() -> Generator[MagicMock]:
"""Return a mocked Saunum client for config flow and integration tests."""
def mock_saunum_client_class() -> Generator[MagicMock]:
"""Return a mocked Saunum client class for config flow and integration tests."""
with (
patch(
"homeassistant.components.saunum.config_flow.SaunumClient", autospec=True
@@ -53,6 +53,8 @@ def mock_saunum_client() -> Generator[MagicMock]:
mock_client = mock_client_class.return_value
mock_client.is_connected = True
mock_client_class.create = AsyncMock(return_value=mock_client)
# Create mock data for async_get_data
mock_data = SaunumData(
session_active=False,
@@ -76,7 +78,13 @@ def mock_saunum_client() -> Generator[MagicMock]:
mock_client.async_get_data.return_value = mock_data
yield mock_client
yield mock_client_class
@pytest.fixture
def mock_saunum_client(mock_saunum_client_class: MagicMock) -> MagicMock:
"""Return a mocked Saunum client instance."""
return mock_saunum_client_class.return_value
@pytest.fixture

View File

@@ -49,14 +49,13 @@ async def test_full_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No
)
async def test_form_errors(
hass: HomeAssistant,
mock_saunum_client,
mock_saunum_client_class,
side_effect: Exception,
error_base: str,
mock_setup_entry: AsyncMock,
) -> None:
"""Test error handling and recovery."""
mock_saunum_client.connect.side_effect = side_effect
mock_saunum_client_class.create.side_effect = side_effect
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
@@ -69,8 +68,8 @@ async def test_form_errors(
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error_base}
# Test recovery - clear the error and try again
mock_saunum_client.connect.side_effect = None
# Test recovery - try again without the error
mock_saunum_client_class.create.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
@@ -139,20 +138,20 @@ async def test_reconfigure_flow(
async def test_reconfigure_errors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_saunum_client,
mock_saunum_client_class,
side_effect: Exception,
error_base: str,
mock_setup_entry: AsyncMock,
) -> None:
"""Test reconfigure flow error handling."""
mock_config_entry.add_to_hass(hass)
mock_saunum_client.connect.side_effect = side_effect
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
mock_saunum_client_class.create.side_effect = side_effect
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
TEST_RECONFIGURE_INPUT,
@@ -161,8 +160,8 @@ async def test_reconfigure_errors(
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": error_base}
# Test recovery - clear the error and try again
mock_saunum_client.connect.side_effect = None
# Test recovery - try again without the error
mock_saunum_client_class.create.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],

View File

@@ -1,5 +1,7 @@
"""Test Saunum Leil integration setup and teardown."""
from unittest.mock import patch
from pysaunum import SaunumConnectionError
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -35,9 +37,11 @@ async def test_async_setup_entry_connection_failed(
"""Test integration setup fails when connection cannot be established."""
mock_config_entry.add_to_hass(hass)
mock_saunum_client.connect.side_effect = SaunumConnectionError("Connection failed")
assert not await hass.config_entries.async_setup(mock_config_entry.entry_id)
with patch(
"homeassistant.components.saunum.SaunumClient.create",
side_effect=SaunumConnectionError("Connection failed"),
):
assert not await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -383,6 +383,13 @@ async def test_smoke_co_notification_sensors(
assert entity_entry
assert entity_entry.entity_category == EntityCategory.DIAGNOSTIC
# Test that no idle states are created as entities
entity_id = "binary_sensor.zcombo_g_smoke_co_alarm_idle"
state = hass.states.get(entity_id)
assert state is None
entity_entry = entity_registry.async_get(entity_id)
assert entity_entry is None
# Test state updates for smoke alarm
event = Event(
type="value updated",