Compare commits

..

64 Commits

Author SHA1 Message Date
Franck Nijhof c576d5267a Bump steamloop to 1.2.1 (#173809) 2026-06-14 19:45:34 -05:00
Franck Nijhof 1241a11c9d Bump bleak-esphome to 3.9.4 (#173825) 2026-06-14 17:56:47 -05:00
Franck Nijhof 8f7447de58 Bump aiocomelit to 2.0.5 (#173800) 2026-06-15 00:00:16 +02:00
Franck Nijhof 1e54dba835 Bump pyiskra to 0.1.29 (#173820) 2026-06-15 00:00:07 +02:00
Manu 20583d6d1b Bump pyrate-limiter to 4.4.0 (#173819) 2026-06-14 23:59:57 +02:00
Franck Nijhof b94370ee51 Bump devolo-home-control-api to 0.19.1 (#173806) 2026-06-14 23:59:51 +02:00
Franck Nijhof d068a2aa11 Bump blinkpy to 0.25.6 (#173811) 2026-06-14 23:59:30 +02:00
Franck Nijhof 5d53a1f204 Bump anthemav to 1.4.2 (#173812) 2026-06-14 23:59:08 +02:00
Franck Nijhof 2908f37130 Bump pyimouapi to 1.2.8 (#173813) 2026-06-14 23:58:45 +02:00
Franck Nijhof a88a795ad3 Bump bluecurrent-api to 1.3.3 (#173815) 2026-06-14 23:58:24 +02:00
Franck Nijhof 73a36a2c47 Bump denonavr to 1.3.3 (#173814) 2026-06-14 23:58:05 +02:00
Franck Nijhof 054494181e Bump geniushub-client to 0.7.4 (#173818) 2026-06-14 23:57:38 +02:00
Franck Nijhof e4e8f901ab Bump yalexs-ble to 3.3.1 (#173792) 2026-06-14 14:58:12 -05:00
Franck Nijhof b7a29bfa2f Bump pyrainbird to 6.3.1 (#173786) 2026-06-14 21:23:59 +02:00
Franck Nijhof 26b0079945 Bump pyenphase to 2.4.9 (#173785) 2026-06-14 14:13:54 -05:00
Raphael Hehl 7454f40dd8 Bump uiprotect to 13.1.2 (#173728) 2026-06-14 19:12:07 +02:00
Franck Nijhof 26b7d1e32c Slugify OwnTracks beacon name in entity ID (#173629) 2026-06-14 19:12:05 +02:00
Franck Nijhof f7342ea9b0 Add missing translation_domain to nasweb exception raises (#173732) 2026-06-14 19:11:42 +02:00
Franck Nijhof 825d99ddaf Bump uv to 0.11.21 (#173768) 2026-06-14 19:09:43 +02:00
Michael 401fae6bdd Bump py-synologydsm-api to 2.10.0 (#173774) 2026-06-14 19:08:49 +02:00
Franck Nijhof 5433beeec1 Skip Miele fan set_percentage when already at the target step (#173725) 2026-06-14 19:06:54 +02:00
G Johansson af60e248d3 Remove listener from holiday calendar when entity is disabled (#173759) 2026-06-14 19:05:33 +02:00
Franck Nijhof 8c452c280f Add missing flow form field translation in hue (#173747) 2026-06-14 19:02:44 +02:00
Franck Nijhof 3aec970321 Add missing flow form field translations in islamic_prayer_times (#173749) 2026-06-14 19:02:03 +02:00
Franck Nijhof 687c91d5f4 Add missing flow form field translation in lacrosse_view (#173750) 2026-06-14 19:01:36 +02:00
Franck Nijhof 377fdceb6c Add missing flow form field translation in melnor (#173752) 2026-06-14 19:00:56 +02:00
Franck Nijhof 11a4533ccc Fix flow form field translation key in meteoclimatic (#173754) 2026-06-14 19:00:12 +02:00
Franck Nijhof 52b2738b2a Add missing flow form field translation in motionblinds_ble (#173758) 2026-06-14 18:59:23 +02:00
Franck Nijhof 3fda722dbb Add missing flow form field translation in blink (#173756) 2026-06-14 18:58:52 +02:00
Franck Nijhof e01215da0e Fix exception translation placeholder mismatch in Swiss Public Transport (#173735) 2026-06-14 18:58:05 +02:00
starkillerOG 6c116cf3e4 Bump reolink_aio to 0.21.1 (#173772) 2026-06-14 18:56:55 +02:00
Franck Nijhof 8017e802dd Bump aiopulse to 0.4.7 (#173763) 2026-06-14 18:56:12 +02:00
Franck Nijhof 501d956b1b Bump pylint to 4.0.6 (#173769) 2026-06-14 18:52:24 +02:00
Franck Nijhof 8aca342a78 Bump snapcast to 2.3.8 (#173765) 2026-06-14 18:51:48 +02:00
Franck Nijhof bd68e9fbe3 Bump syrupy to 5.3.2 (#173767) 2026-06-14 18:51:12 +02:00
Franck Nijhof b75c839868 Bump python-linkplay to 0.2.14 (#173770) 2026-06-14 18:49:59 +02:00
Franck Nijhof 742bfb00ff Bump anova-wifi to 0.17.1 (#173764) 2026-06-14 18:49:24 +02:00
Franck Nijhof 987c19d991 Replace duplicate SERVICE_RELOAD constant with homeassistant.const import in conversation (#173741) 2026-06-14 18:48:55 +02:00
Franck Nijhof b4319c4d0c Bump aioacaia to 0.1.18 (#173762) 2026-06-14 18:35:35 +02:00
Franck Nijhof 0fdb3ebed7 Bump aioamazondevices to 14.0.4 (#173761) 2026-06-14 18:16:06 +02:00
G Johansson efa3334616 Bump lxml to 6.1.1 (#173748) 2026-06-14 18:00:43 +02:00
Franck Nijhof 9ec0f2fe4f Add missing flow form field translation in gogogate2 (#173753) 2026-06-14 17:30:29 +02:00
Franck Nijhof 9bc5e2b06b Fix flow form field translation key in lookin (#173751) 2026-06-14 17:30:21 +02:00
Franck Nijhof 46a38cc481 Add missing flow form field translation in flux_led (#173746) 2026-06-14 17:30:13 +02:00
Franck Nijhof a63f2f1d20 Replace duplicate constants with homeassistant.const imports in Teslemetry (#173744) 2026-06-14 17:26:39 +02:00
Franck Nijhof 744bb6a068 Add missing flow form field translation in tuya (#173745) 2026-06-14 17:26:16 +02:00
Franck Nijhof d449e3e97b Replace duplicate constants with homeassistant.const imports in select (#173743) 2026-06-14 17:25:53 +02:00
Franck Nijhof 0df379704f Replace duplicate CONF_OPTIONS constant with homeassistant.const import in input_select (#173742) 2026-06-14 17:25:24 +02:00
Franck Nijhof 4ab7ce04a8 Add missing exception translation key in Israel Rail (#173738) 2026-06-14 17:24:54 +02:00
Franck Nijhof 210b08b637 Fix exception translation placeholder mismatch in Homevolt (#173737) 2026-06-14 17:24:31 +02:00
Franck Nijhof f0b448dc6e Fix exception translation placeholder mismatch in Snoo (#173736) 2026-06-14 17:24:04 +02:00
Franck Nijhof b5a314bf60 Add missing exception translation keys in VeSync (#173739) 2026-06-14 17:23:38 +02:00
Franck Nijhof 741c342749 Replace duplicate CONF_EVENT constant with homeassistant.const import in calendar (#173740) 2026-06-14 17:23:11 +02:00
Franck Nijhof f4d4df9c35 Fix options flow form field translation key in plaato (#173755) 2026-06-14 17:22:07 +02:00
Franck Nijhof bcbdf7b2bb Add missing flow form field translation in snooz (#173760) 2026-06-14 17:21:15 +02:00
iluebbe b3309ef169 Add Powerline hint to username field description (#167473)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-14 16:57:21 +02:00
epenet caaf5f9715 Adjust pylint checker to prevent invalid use of Platform enum (#173374)
Co-authored-by: Markus Tuominen <3738613+Markus98@users.noreply.github.com>
2026-06-14 16:17:00 +02:00
BrettLynch123 7ce7de3650 Fix tessie setup_error on transient aiohttp.ClientError during startup (#173659) 2026-06-14 15:49:22 +02:00
fdebrus 2c14c6be75 Optimistic UI updates for Vistapool write entities (#173373)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-14 15:41:57 +02:00
Christian Lackas e020f338ab Add window state sensor for HomematicIP rotary handle (HmIP-SRH) (#173423) 2026-06-14 15:36:15 +02:00
jasonjhofmann c85c2c4cd3 Add network MAC connection to JVC Projector device (#173683)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:56:29 +02:00
Raman Gupta c4e618e990 Stop validating # of slots in zwave_js.set_credential action (#173644) 2026-06-14 14:18:32 +02:00
Åke Strandberg 5efde60d21 Remove unnecessary #pylint disable..." (#173726) 2026-06-14 14:17:16 +02:00
jasonjhofmann d9dc10ed81 Add network MAC connection to myStrom bulb devices (#173707)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:16:50 +02:00
108 changed files with 907 additions and 356 deletions
+1 -1
View File
@@ -26,5 +26,5 @@
"iot_class": "local_push",
"loggers": ["aioacaia"],
"quality_scale": "platinum",
"requirements": ["aioacaia==0.1.17"]
"requirements": ["aioacaia==0.1.18"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/acmeda",
"iot_class": "local_push",
"loggers": ["aiopulse"],
"requirements": ["aiopulse==0.4.6"]
"requirements": ["aiopulse==0.4.7"]
}
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==14.0.3"]
"requirements": ["aioamazondevices==14.0.4"]
}
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["anova_wifi"],
"requirements": ["anova-wifi==0.17.0"]
"requirements": ["anova-wifi==0.17.1"]
}
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["anthemav"],
"requirements": ["anthemav==1.4.1"]
"requirements": ["anthemav==1.4.2"]
}
@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.7", "yalexs-ble==3.3.0"]
"requirements": ["yalexs==9.2.7", "yalexs-ble==3.3.1"]
}
+1 -1
View File
@@ -21,5 +21,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["blinkpy"],
"requirements": ["blinkpy==0.25.2"]
"requirements": ["blinkpy==0.25.6"]
}
@@ -26,6 +26,12 @@
"description": "The credentials for {username} need to be updated",
"title": "Re-authenticate Blink"
},
"reconfigure": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
}
},
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["bluecurrent_api"],
"requirements": ["bluecurrent-api==1.3.2"]
"requirements": ["bluecurrent-api==1.3.3"]
}
@@ -24,7 +24,7 @@ from homeassistant.components.websocket_api import (
ActiveConnection,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.const import CONF_EVENT, STATE_OFF, STATE_ON
from homeassistant.core import (
CALLBACK_TYPE,
HomeAssistant,
@@ -45,7 +45,6 @@ from homeassistant.util import dt as dt_util
from homeassistant.util.json import JsonValueType
from .const import (
CONF_EVENT,
DATA_COMPONENT,
DOMAIN,
EVENT_DESCRIPTION,
@@ -13,9 +13,6 @@ if TYPE_CHECKING:
DOMAIN = "calendar"
DATA_COMPONENT: HassKey[EntityComponent[CalendarEntity]] = HassKey(DOMAIN)
# pylint: disable-next=home-assistant-duplicate-const
CONF_EVENT = "event"
class CalendarEntityFeature(IntFlag):
"""Supported features of the calendar entity."""
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "platinum",
"requirements": ["aiocomelit==2.0.3"]
"requirements": ["aiocomelit==2.0.5"]
}
@@ -8,7 +8,7 @@ from hassil.recognize import RecognizeResult
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import MATCH_ALL
from homeassistant.const import MATCH_ALL, SERVICE_RELOAD
from homeassistant.core import (
HomeAssistant,
ServiceCall,
@@ -53,7 +53,6 @@ from .const import (
METADATA_CUSTOM_FILE,
METADATA_CUSTOM_SENTENCE,
SERVICE_PROCESS,
SERVICE_RELOAD,
ConversationEntityFeature,
)
from .default_agent import async_setup_default_agent
@@ -19,8 +19,6 @@ ATTR_AGENT_ID = "agent_id"
ATTR_CONVERSATION_ID = "conversation_id"
SERVICE_PROCESS = "process"
# pylint: disable-next=home-assistant-duplicate-const
SERVICE_RELOAD = "reload"
DATA_COMPONENT: HassKey[EntityComponent[ConversationEntity]] = HassKey(DOMAIN)
@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.3.2"],
"requirements": ["denonavr==1.3.3"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
@@ -9,6 +9,6 @@
"iot_class": "local_push",
"loggers": ["HomeControl", "Mydevolo", "MprmRest", "MprmWebsocket", "Mprm"],
"quality_scale": "silver",
"requirements": ["devolo-home-control-api==0.19.0"],
"requirements": ["devolo-home-control-api==0.19.1"],
"zeroconf": ["_dvl-deviceapi._tcp.local."]
}
@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.4.8"],
"requirements": ["pyenphase==2.4.9"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."
@@ -19,7 +19,7 @@
"requirements": [
"aioesphomeapi==45.3.1",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.9.1"
"bleak-esphome==3.9.4"
],
"zeroconf": ["_esphomelib._tcp.local."]
}
@@ -13,6 +13,11 @@
"discovery_confirm": {
"description": "Do you want to set up {model} {id} ({ipaddr})?"
},
"pick_device": {
"data": {
"device": "[%key:common::config_flow::data::device%]"
}
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
+1 -1
View File
@@ -5,7 +5,7 @@
"data_description_password": "Password for the FRITZ!Box.",
"data_description_port": "Leave empty to use the default port.",
"data_description_ssl": "Use SSL to connect to the FRITZ!Box.",
"data_description_username": "Username for the FRITZ!Box.",
"data_description_username": "Username for the FRITZ!Box. FRITZ!Powerline devices ignore this information and accept any value.",
"data_feature_device_tracking": "Enable network device tracking"
},
"config": {
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/geniushub",
"iot_class": "local_polling",
"loggers": ["geniushubclient"],
"requirements": ["geniushub-client==0.7.1"]
"requirements": ["geniushub-client==0.7.4"]
}
@@ -11,6 +11,7 @@
"step": {
"user": {
"data": {
"device": "[%key:common::config_flow::data::device%]",
"ip_address": "[%key:common::config_flow::data::ip%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
@@ -151,6 +151,13 @@ class HolidayCalendarEntity(CalendarEntity):
"""Set up first update."""
self._update_state_and_setup_listener()
async def async_will_remove_from_hass(self) -> None:
"""Cancel listener when removing."""
await super().async_will_remove_from_hass()
if self.unsub:
self.unsub()
self.unsub = None
def update_event(self, now: datetime) -> CalendarEvent | None:
"""Return the next upcoming event."""
next_holiday = None
@@ -27,6 +27,7 @@ from homematicip.device import (
PassageDetector,
PresenceDetectorIndoor,
RoomControlDeviceAnalog,
RotaryHandleSensor,
SmokeDetector,
SoilMoistureSensorInterface,
SwitchMeasuring,
@@ -166,6 +167,7 @@ ILLUMINATION_DEVICE_ATTRIBUTES = {
}
TILT_STATE_VALUES = ["neutral", "tilted", "non_neutral"]
WINDOW_STATE_VALUES = ["open", "closed", "tilted"]
def get_device_handlers(hap: HomematicipHAP) -> dict[type, Callable]:
@@ -204,6 +206,9 @@ def get_device_handlers(hap: HomematicipHAP) -> dict[type, Callable]:
RoomControlDeviceAnalog: lambda device: [
HomematicipTemperatureSensor(hap, device),
],
RotaryHandleSensor: lambda device: [
HomematicipWindowStateSensor(hap, device),
],
LightSensor: lambda device: [
HomematicipIlluminanceSensor(hap, device),
],
@@ -498,6 +503,24 @@ class HomematicipTiltStateSensor(HomematicipGenericEntity, SensorEntity):
return state_attr
class HomematicipWindowStateSensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP rotary handle window state sensor."""
_attr_device_class = SensorDeviceClass.ENUM
_attr_options = WINDOW_STATE_VALUES
_attr_translation_key = "window_state"
def __init__(self, hap: HomematicipHAP, device: RotaryHandleSensor) -> None:
"""Initialize the window state sensor."""
super().__init__(hap, device, feature_id="window_state")
@property
def native_value(self) -> str | None:
"""Return the state."""
window_state = getattr(self._device, "windowState", None)
return window_state.lower() if window_state is not None else None
class HomematicipFloorTerminalBlockMechanicChannelValve(
HomematicipGenericEntity, SensorEntity
):
@@ -98,6 +98,14 @@
"non_neutral": "Non-neutral",
"tilted": "Tilted"
}
},
"window_state": {
"name": "Window state",
"state": {
"closed": "[%key:common::state::closed%]",
"open": "[%key:common::state::open%]",
"tilted": "Tilted"
}
}
}
},
@@ -50,14 +50,12 @@ def homevolt_exception_handler[_HomevoltEntityT: HomevoltEntity, **_P](
translation_key="auth_failed",
) from error
except HomevoltConnectionError as error:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
translation_placeholders={"error": str(error)},
) from error
except HomevoltError as error:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="unknown_error",
@@ -172,10 +172,10 @@
"message": "[%key:common::config_flow::error::invalid_auth%]"
},
"communication_error": {
"message": "[%key:common::config_flow::error::cannot_connect%]"
"message": "Error communicating with the Homevolt battery: {error}"
},
"unknown_error": {
"message": "[%key:common::config_flow::error::unknown%]"
"message": "Unknown error from the Homevolt battery: {error}"
}
}
}
+2 -1
View File
@@ -18,7 +18,8 @@
"step": {
"init": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
"host": "[%key:common::config_flow::data::host%]",
"id": "Hue bridge"
},
"data_description": {
"host": "The hostname or IP address of your Hue bridge."
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["pyimouapi==1.2.7"]
"requirements": ["pyimouapi==1.2.8"]
}
@@ -21,6 +21,7 @@ from homeassistant.const import (
CONF_ICON,
CONF_ID,
CONF_NAME,
CONF_OPTIONS,
SERVICE_RELOAD,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
@@ -37,8 +38,6 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "input_select"
CONF_INITIAL = "initial"
# pylint: disable-next=home-assistant-duplicate-const
CONF_OPTIONS = "options"
SERVICE_SET_OPTIONS = "set_options"
STORAGE_KEY = DOMAIN
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyiskra"],
"requirements": ["pyiskra==0.1.27"]
"requirements": ["pyiskra==0.1.29"]
}
@@ -5,6 +5,10 @@
},
"step": {
"user": {
"data": {
"location": "[%key:common::config_flow::data::location%]",
"name": "[%key:common::config_flow::data::name%]"
},
"description": "Do you want to set up Islamic Prayer Times?",
"title": "Set up Islamic Prayer Times"
}
@@ -29,7 +29,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: IsraelRailConfigEntry) -
try:
await hass.async_add_executor_job(train_schedule.query, start, destination)
except Exception as e:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="request_timeout",
@@ -65,5 +65,10 @@
"name": "Trains +2"
}
}
},
"exceptions": {
"request_timeout": {
"message": "Timeout connecting to the Israel Rail API for {config_title}: {error}"
}
}
}
@@ -4,7 +4,7 @@ import logging
from jvcprojector import Command, JvcProjector
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER, NAME
@@ -27,8 +27,12 @@ class JvcProjectorEntity(CoordinatorEntity[JvcProjectorDataUpdateCoordinator]):
super().__init__(coordinator, command)
self._attr_unique_id = coordinator.unique_id
# The config entry unique id is the device's formatted MAC address (set
# from the projector's MAC in the config flow), so it doubles as the
# network MAC connection.
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
connections={(CONNECTION_NETWORK_MAC, self._attr_unique_id)},
name=NAME,
model=self.device.model,
manufacturer=MANUFACTURER,
@@ -10,6 +10,11 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"location": {
"data": {
"location": "[%key:common::config_flow::data::location%]"
}
},
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["linkplay"],
"requirements": ["python-linkplay==0.2.12"],
"requirements": ["python-linkplay==0.2.14"],
"zeroconf": ["_linkplay._tcp.local."]
}
+1 -1
View File
@@ -23,7 +23,7 @@
},
"user": {
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]"
"host": "[%key:common::config_flow::data::host%]"
}
}
}
@@ -8,6 +8,11 @@
"bluetooth_confirm": {
"description": "Do you want to add the Melnor Bluetooth valve `{name}` to Home Assistant?",
"title": "Discovered Melnor Bluetooth valve"
},
"pick_device": {
"data": {
"address": "[%key:common::config_flow::data::device%]"
}
}
}
},
@@ -10,10 +10,10 @@
"step": {
"user": {
"data": {
"code": "Station code"
"station_code": "Station code"
},
"data_description": {
"code": "Looks like ESCAT4300000043206B"
"station_code": "Looks like ESCAT4300000043206B"
}
}
}
+2 -2
View File
@@ -135,6 +135,8 @@ class MieleFan(MieleEntity, FanEntity):
_LOGGER.debug("Calc ventilation_step: %s", ventilation_step)
if ventilation_step == 0:
await self.async_turn_off()
elif ventilation_step == self.device.state_ventilation_step:
return
else:
try:
await self.api.send_action(
@@ -165,7 +167,6 @@ class MieleFan(MieleEntity, FanEntity):
try:
await self.api.send_action(self._device_id, {POWER_ON: True})
except ClientResponseError as ex:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_state_error",
@@ -183,7 +184,6 @@ class MieleFan(MieleEntity, FanEntity):
try:
await self.api.send_action(self._device_id, {POWER_OFF: True})
except ClientResponseError as ex:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_state_error",
@@ -1,15 +1,14 @@
"""The Modern Forms integration."""
from collections.abc import Callable, Coroutine
import logging
from typing import Any, Concatenate
from aiomodernforms import ModernFormsConnectionError, ModernFormsError
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
@@ -20,6 +19,7 @@ PLATFORMS = [
Platform.SENSOR,
Platform.SWITCH,
]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ModernFormsConfigEntry) -> bool:
@@ -53,7 +53,7 @@ def modernforms_exception_handler[
"""Decorate Modern Forms calls to handle Modern Forms exceptions.
A decorator that wraps the passed in function, catches Modern Forms errors,
and raises translated HomeAssistantError exceptions.
and handles the availability of the device in the data coordinator.
"""
async def handler(
@@ -64,17 +64,11 @@ def modernforms_exception_handler[
self.coordinator.async_update_listeners()
except ModernFormsConnectionError as error:
_LOGGER.error("Error communicating with API: %s", error)
self.coordinator.last_update_success = False
self.coordinator.async_update_listeners()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="communication_error",
) from error
except ModernFormsError as error:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="invalid_response",
) from error
_LOGGER.error("Invalid response from API: %s", error)
return handler
@@ -108,11 +108,13 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity):
"""Return the state of the fan."""
return bool(self.coordinator.data.state.fan_on)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
await self.coordinator.modern_forms.fan(direction=direction)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed percentage of the fan."""
@@ -121,6 +123,7 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity):
else:
await self.async_turn_off()
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_turn_on(
self,
@@ -137,11 +140,13 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity):
)
await self.coordinator.modern_forms.fan(**data)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fan off."""
await self.coordinator.modern_forms.fan(on=FAN_POWER_OFF)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_set_fan_sleep_timer(
self,
@@ -150,6 +155,7 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity):
"""Set a Modern Forms light sleep timer."""
await self.coordinator.modern_forms.fan(sleep=sleep_time * 60)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_clear_fan_sleep_timer(
self,
@@ -100,11 +100,13 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity):
"""Return the state of the light."""
return bool(self.coordinator.data.state.light_on)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
await self.coordinator.modern_forms.light(on=LIGHT_POWER_OFF)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
@@ -117,6 +119,7 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity):
await self.coordinator.modern_forms.light(**data)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_set_light_sleep_timer(
self,
@@ -125,6 +128,7 @@ class ModernFormsLightEntity(ModernFormsDeviceEntity, LightEntity):
"""Set a Modern Forms light sleep timer."""
await self.coordinator.modern_forms.light(sleep=sleep_time * 60)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_clear_light_sleep_timer(
self,
@@ -60,14 +60,6 @@
}
}
},
"exceptions": {
"communication_error": {
"message": "Error communicating with the Modern Forms device"
},
"invalid_response": {
"message": "Invalid response from the Modern Forms device"
}
},
"services": {
"clear_fan_sleep_timer": {
"description": "Clears the sleep timer on a Modern Forms fan.",
@@ -62,11 +62,13 @@ class ModernFormsAwaySwitch(ModernFormsSwitch):
"""Return the state of the switch."""
return bool(self.coordinator.data.state.away_mode_enabled)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the Modern Forms Away mode switch."""
await self.coordinator.modern_forms.away(away=False)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the Modern Forms Away mode switch."""
@@ -93,11 +95,13 @@ class ModernFormsAdaptiveLearningSwitch(ModernFormsSwitch):
"""Return the state of the switch."""
return bool(self.coordinator.data.state.adaptive_learning_enabled)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the Modern Forms Adaptive Learning switch."""
await self.coordinator.modern_forms.adaptive_learning(adaptive_learning=False)
# pylint: disable-next=home-assistant-action-swallowed-exception
@modernforms_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the Modern Forms Adaptive Learning switch."""
@@ -10,6 +10,9 @@
},
"step": {
"confirm": {
"data": {
"blind_type": "Blind type"
},
"description": "What kind of blind is {display_name}?"
},
"user": {
+2 -1
View File
@@ -14,7 +14,7 @@ from homeassistant.components.light import (
LightEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, MANUFACTURER
@@ -57,6 +57,7 @@ class MyStromLight(LightEntity):
self._attr_hs_color = 0, 0
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, mac)},
connections={(CONNECTION_NETWORK_MAC, mac)},
name=name,
manufacturer=MANUFACTURER,
sw_version=self._bulb.firmware,
+12 -10
View File
@@ -50,16 +50,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: NASwebConfigEntry) -> bo
)
if not await webio_api.refresh_device_info():
_LOGGER.error("[%s] Refresh device info failed", entry.data[CONF_HOST])
# pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="config_entry_error_internal_error",
translation_placeholders={"support_email": SUPPORT_EMAIL},
)
webio_serial = webio_api.get_serial_number()
if webio_serial is None:
_LOGGER.error("[%s] Serial number not available", entry.data[CONF_HOST])
# pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="config_entry_error_internal_error",
translation_placeholders={"support_email": SUPPORT_EMAIL},
)
@@ -67,8 +67,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: NASwebConfigEntry) -> bo
_LOGGER.error(
"[%s] Serial number doesn't match config entry", entry.data[CONF_HOST]
)
# pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch
raise ConfigEntryError(translation_key="config_entry_error_serial_mismatch")
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="config_entry_error_serial_mismatch",
)
coordinator = NASwebCoordinator(
hass, webio_api, name=f"NASweb[{webio_api.get_name()}]"
@@ -79,15 +81,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: NASwebConfigEntry) -> bo
webhook_url = nasweb_data.get_webhook_url(hass)
if not await webio_api.status_subscription(webhook_url, True):
_LOGGER.error("Failed to subscribe for status updates from webio")
# pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="config_entry_error_internal_error",
translation_placeholders={"support_email": SUPPORT_EMAIL},
)
if not await nasweb_data.notify_coordinator.check_connection(webio_serial):
_LOGGER.error("Did not receive status from device")
# pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="config_entry_error_no_status_update",
translation_placeholders={"support_email": SUPPORT_EMAIL},
)
@@ -96,14 +98,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: NASwebConfigEntry) -> bo
f"[{entry.data[CONF_HOST]}] Check connection reached timeout"
) from error
except AuthError as error:
# pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch
raise ConfigEntryError(
translation_key="config_entry_error_invalid_authentication"
translation_domain=DOMAIN,
translation_key="config_entry_error_invalid_authentication",
) from error
except NoURLAvailableError as error:
# pylint: disable-next=home-assistant-exception-translation-key-domain-mismatch
raise ConfigEntryError(
translation_key="config_entry_error_missing_internal_url"
translation_domain=DOMAIN,
translation_key="config_entry_error_missing_internal_url",
) from error
device_registry = dr.async_get(hass)
+1 -1
View File
@@ -65,7 +65,7 @@
"config_entry_error_no_status_update": {
"message": "Did not receive any status updates within the expected time window. Make sure the Home Assistant internal URL is reachable from the NASweb device. If the issue persists contact support at {support_email}"
},
"serial_mismatch": {
"config_entry_error_serial_mismatch": {
"message": "Connected to different NASweb device (serial number mismatch)."
}
}
@@ -26,6 +26,7 @@ from homeassistant.helpers.dispatcher import (
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import async_when_setup
from homeassistant.util import slugify
from homeassistant.util.json import json_loads
from .config_flow import CONF_SECRET
@@ -311,6 +312,6 @@ class OwnTracksContext:
# kwargs location is the beacon's configured lat/lon
kwargs.pop("battery", None)
for beacon in self.mobile_beacons_active[dev_id]:
kwargs["dev_id"] = f"{BEACON_DEV_ID}_{beacon}"
kwargs["dev_id"] = slugify(f"{BEACON_DEV_ID}_{beacon}")
kwargs["host_name"] = beacon
self.async_see(**kwargs)
+1 -1
View File
@@ -41,7 +41,7 @@
"step": {
"user": {
"data": {
"update_interval": "Update interval (minutes)"
"scan_interval": "Update interval (minutes)"
},
"description": "Set the update interval (minutes)",
"title": "Options for Plaato"
@@ -90,5 +90,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["PSNAWP==3.0.3", "pyrate-limiter==4.2.0"]
"requirements": ["PSNAWP==3.0.3", "pyrate-limiter==4.4.0"]
}
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyrainbird"],
"requirements": ["pyrainbird==6.3.0"]
"requirements": ["pyrainbird==6.3.1"]
}
@@ -20,5 +20,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.21.0"]
"requirements": ["reolink-aio==0.21.1"]
}
@@ -6,5 +6,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/scrape",
"iot_class": "cloud_polling",
"requirements": ["beautifulsoup4==4.13.3", "lxml==6.0.1"]
"requirements": ["beautifulsoup4==4.13.3", "lxml==6.1.1"]
}
+1 -2
View File
@@ -8,6 +8,7 @@ from propcache.api import cached_property
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_OPTION, SERVICE_SELECT_OPTION
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
@@ -18,13 +19,11 @@ from homeassistant.util.hass_dict import HassKey
from .const import (
ATTR_CYCLE,
ATTR_OPTION,
ATTR_OPTIONS,
DOMAIN,
SERVICE_SELECT_FIRST,
SERVICE_SELECT_LAST,
SERVICE_SELECT_NEXT,
SERVICE_SELECT_OPTION,
SERVICE_SELECT_PREVIOUS,
)
-4
View File
@@ -4,8 +4,6 @@ DOMAIN = "select"
ATTR_CYCLE = "cycle"
ATTR_OPTIONS = "options"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_OPTION = "option"
CONF_CYCLE = "cycle"
CONF_OPTION = "option"
@@ -13,6 +11,4 @@ CONF_OPTION = "option"
SERVICE_SELECT_FIRST = "select_first"
SERVICE_SELECT_LAST = "select_last"
SERVICE_SELECT_NEXT = "select_next"
# pylint: disable-next=home-assistant-duplicate-const
SERVICE_SELECT_OPTION = "select_option"
SERVICE_SELECT_PREVIOUS = "select_previous"
@@ -10,10 +10,12 @@ from homeassistant.components.device_automation import (
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_OPTION,
CONF_DEVICE_ID,
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_TYPE,
SERVICE_SELECT_OPTION,
)
from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -23,7 +25,6 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from .const import (
ATTR_CYCLE,
ATTR_OPTION,
ATTR_OPTIONS,
CONF_CYCLE,
CONF_OPTION,
@@ -31,7 +32,6 @@ from .const import (
SERVICE_SELECT_FIRST,
SERVICE_SELECT_LAST,
SERVICE_SELECT_NEXT,
SERVICE_SELECT_OPTION,
SERVICE_SELECT_PREVIOUS,
)
@@ -5,10 +5,10 @@ from collections.abc import Iterable
import logging
from typing import Any
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_SELECT_OPTION
from homeassistant.core import Context, HomeAssistant, State
from .const import ATTR_OPTION, ATTR_OPTIONS, DOMAIN, SERVICE_SELECT_OPTION
from .const import ATTR_OPTIONS, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["construct", "snapcast"],
"requirements": ["snapcast==2.3.7"]
"requirements": ["snapcast==2.3.8"]
}
+2 -4
View File
@@ -80,11 +80,10 @@ class SnooSwitch(SnooDescriptionEntity, SwitchEntity):
True,
)
except SnooCommandException as err:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="switch_on_failed",
translation_placeholders={"name": str(self.name), "status": "on"},
translation_placeholders={"name": str(self.name)},
) from err
async def async_turn_off(self, **kwargs: Any) -> None:
@@ -97,9 +96,8 @@ class SnooSwitch(SnooDescriptionEntity, SwitchEntity):
False,
)
except SnooCommandException as err:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="switch_off_failed",
translation_placeholders={"name": str(self.name), "status": "off"},
translation_placeholders={"name": str(self.name)},
) from err
+2 -1
View File
@@ -18,7 +18,8 @@
},
"user": {
"data": {
"address": "[%key:common::config_flow::data::device%]"
"address": "[%key:common::config_flow::data::device%]",
"name": "[%key:common::config_flow::data::name%]"
},
"description": "[%key:component::bluetooth::config::step::user::description%]"
}
@@ -86,12 +86,11 @@ async def async_setup_entry(
},
) from e
except OpendataTransportError as e:
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="invalid_data",
translation_placeholders={
**PLACEHOLDERS,
"stationboard_url": PLACEHOLDERS["stationboard_url"],
"config_title": entry.title,
"error": str(e),
},
@@ -8,7 +8,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["synology_dsm"],
"requirements": ["py-synologydsm-api==2.9.0"],
"requirements": ["py-synologydsm-api==2.10.0"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:Basic:1",
@@ -6,7 +6,15 @@ import voluptuous as vol
from voluptuous import All, Range
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_ID, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.const import (
ATTR_ID,
ATTR_LOCATION,
ATTR_NAME,
ATTR_TIME,
CONF_DEVICE_ID,
CONF_LATITUDE,
CONF_LONGITUDE,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
@@ -18,20 +26,14 @@ from .models import TeslemetryEnergyData, TeslemetryVehicleData
_LOGGER = logging.getLogger(__name__)
# Attributes
# pylint: disable-next=home-assistant-duplicate-const
ATTR_ID = "id"
ATTR_GPS = "gps"
ATTR_TYPE = "type"
ATTR_VALUE = "value"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_LOCATION = "location"
ATTR_LOCALE = "locale"
ATTR_ORDER = "order"
ATTR_TIMESTAMP = "timestamp"
ATTR_FIELDS = "fields"
ATTR_ENABLE = "enable"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_TIME = "time"
ATTR_PIN = "pin"
ATTR_TOU_SETTINGS = "tou_settings"
ATTR_PRECONDITIONING_ENABLED = "preconditioning_enabled"
@@ -44,8 +46,6 @@ ATTR_DAYS_OF_WEEK = "days_of_week"
ATTR_START_TIME = "start_time"
ATTR_END_TIME = "end_time"
ATTR_ONE_TIME = "one_time"
# pylint: disable-next=home-assistant-duplicate-const
ATTR_NAME = "name"
ATTR_PRECONDITION_TIME = "precondition_time"
# Services
+8 -3
View File
@@ -3,6 +3,7 @@
import asyncio
import logging
from aiohttp import ClientError
from tesla_fleet_api.const import Scope
from tesla_fleet_api.exceptions import (
Forbidden,
@@ -81,6 +82,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from e
except ClientError as e:
raise ConfigEntryNotReady from e
vehicles: list[TessieVehicleData] = []
for vehicle in state_of_all_vehicles["results"]:
@@ -124,13 +127,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
try:
scopes = await tessie.scopes()
except TeslaFleetError as e:
except (TeslaFleetError, ClientError) as e:
raise ConfigEntryNotReady from e
if Scope.ENERGY_DEVICE_DATA in scopes:
try:
products = (await tessie.products())["response"]
except TeslaFleetError as e:
except (TeslaFleetError, ClientError) as e:
raise ConfigEntryNotReady from e
for product in products:
@@ -154,7 +157,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
except (InvalidToken, Forbidden, SubscriptionRequired) as e:
raise ConfigEntryAuthFailed from e
except TeslaFleetError as e:
raise ConfigEntryNotReady(e.message) from e
raise ConfigEntryNotReady(getattr(e, "message", str(e))) from e
except ClientError as e:
raise ConfigEntryNotReady from e
powerwall = (
product["components"]["battery"] or product["components"]["solar"]
+1 -1
View File
@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["steamloop"],
"quality_scale": "bronze",
"requirements": ["steamloop==1.2.0"]
"requirements": ["steamloop==1.2.1"]
}
@@ -15,6 +15,9 @@
"description": "The Tuya integration now uses an improved login method. To reauthenticate with your Smart Life or Tuya Smart account, you need to enter your user code.\n\nYou can find this code in the Smart Life app or Tuya Smart app in **Settings** > **Account and Security** screen, and enter the code shown on the **User Code** field. The user code is case-sensitive, please be sure to enter it exactly as shown in the app."
},
"scan": {
"data": {
"QR": "QR code"
},
"description": "Use the Smart Life app or Tuya Smart app to scan the following QR code to complete the login.\n\nContinue to the next step once you have completed this step in the app."
},
"user": {
@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["uiprotect"],
"quality_scale": "platinum",
"requirements": ["uiprotect==13.1.1"]
"requirements": ["uiprotect==13.1.2"]
}
@@ -64,17 +64,14 @@ async def async_setup_entry(
try:
await manager.login()
except VeSyncLoginError as err:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="invalid_auth"
) from err
except VeSyncServerError as err:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryNotReady(
translation_domain=DOMAIN, translation_key="server_error"
) from err
except VeSyncAPIResponseError as err:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryNotReady(
translation_domain=DOMAIN, translation_key="api_response_error"
) from err
@@ -154,6 +154,17 @@
}
}
},
"exceptions": {
"api_response_error": {
"message": "Invalid response from the VeSync API"
},
"invalid_auth": {
"message": "[%key:common::config_flow::error::invalid_auth%]"
},
"server_error": {
"message": "Server error occurred while connecting to VeSync"
}
},
"services": {
"update_devices": {
"description": "Adds new VeSync devices to Home Assistant.",
+1 -4
View File
@@ -67,7 +67,4 @@ class VistapoolLEDPulseButton(VistapoolEntity, ButtonEntity):
translation_key="set_failed",
translation_placeholders={"entity": self.entity_id},
) from err
# Optimistically reflect the just-written value so a rapid second press
# doesn't read the stale off-state before the Firestore push round-trips.
self.coordinator.data.setdefault("light", {})["status"] = 1
self.coordinator.async_set_updated_data(self.coordinator.data)
self.coordinator.apply_optimistic(_LIGHT_STATUS_PATH, 1)
@@ -81,3 +81,22 @@ class VistapoolDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
def get_value(self, path: str, default: Any = None) -> Any:
"""Get nested data using dot-notation path."""
return AquariteClient.get_value(self.data, path, default)
def apply_optimistic(self, value_path: str, value: Any) -> None:
"""Reflect a just-written value before the Firestore push round-trips.
Hayward's cloud takes several seconds to acknowledge a write back
through Firestore, which would make the UI feel laggy. Writing into
coordinator.data after a successful REST call gives entities instant
feedback; the next snapshot from Firestore overwrites it harmlessly.
"""
keys = value_path.split(".")
target: dict[str, Any] = self.data
for key in keys[:-1]:
child = target.get(key)
if not isinstance(child, dict):
child = {}
target[key] = child
target = child
target[keys[-1]] = value
self.async_set_updated_data(self.data)
@@ -71,3 +71,4 @@ class VistapoolLight(VistapoolEntity, LightEntity):
translation_key="set_failed",
translation_placeholders={"entity": self.entity_id},
) from err
self.coordinator.apply_optimistic(_VALUE_PATH, value)
@@ -233,3 +233,4 @@ class VistapoolNumber(VistapoolEntity, NumberEntity):
translation_key="set_failed",
translation_placeholders={"entity": self.entity_id},
) from err
self.coordinator.apply_optimistic(self.entity_description.value_path, raw)
+1 -1
View File
@@ -14,5 +14,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["socketio", "engineio", "yalexs"],
"requirements": ["yalexs==9.2.7", "yalexs-ble==3.3.0"]
"requirements": ["yalexs==9.2.7", "yalexs-ble==3.3.1"]
}
@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["yalexs-ble==3.3.0"]
"requirements": ["yalexs-ble==3.3.1"]
}
@@ -432,15 +432,6 @@ async def async_set_credential(
translation_key="no_available_credential_slots",
translation_placeholders={"credential_type": cred_type_str},
)
elif not 1 <= credential_slot <= type_cap.number_of_credential_slots:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="credential_slot_out_of_range",
translation_placeholders={
"credential_type": cred_type_str,
"max_slot": str(type_cap.number_of_credential_slots),
},
)
status = await node.access_control.set_credential(
user_id, credential_type, credential_slot, credential_data
@@ -322,9 +322,6 @@
"credential_rejected_wrong_uuid": {
"message": "The device rejected the credential because the user unique identifier does not match."
},
"credential_slot_out_of_range": {
"message": "Credential slot for {credential_type} must be between 1 and {max_slot}."
},
"credential_type_not_supported": {
"message": "Credential type {credential_type} is not supported on this device"
},
+1 -1
View File
@@ -70,7 +70,7 @@ standard-telnetlib==3.13.0
typing-extensions>=4.15.0,<5.0
ulid-transform==2.2.9
urllib3>=2.0
uv==0.11.19
uv==0.11.21
voluptuous-openapi==0.3.0
voluptuous-serialize==2.7.0
voluptuous==0.15.2
@@ -1,4 +1,8 @@
"""Plugin to encourage correct use of DOMAIN constants in tests."""
"""Plugin to prevent incorrect use of Platform enum in tests.
This plugin checks for common test helper functions and methods
where a domain is expected, and ensures the argument is not a Platform.
"""
from dataclasses import dataclass
@@ -30,9 +34,6 @@ _METHOD_CHECKS: list[tuple[str, str, ArgumentCheckInfo]] = [
("hass.states", "async_entity_ids", ArgumentCheckInfo(0, "domain_filter", True)),
]
_DOMAIN_CONSTANTS: set[str] = {"DOMAIN", "domain"}
_DOMAIN_SUFFIXES: tuple[str, ...] = ("_DOMAIN", "_domain")
def _check_call_node_domain_arguments(node: nodes.Call) -> nodes.NodeNG | None:
"""Ensure the call node arguments are valid domain constant or variable.
@@ -81,10 +82,14 @@ def _check_call_node_domain_argument(
return None
def _check_domain_argument(arg_node: nodes.NodeNG, allow_iterable: bool) -> bool:
def _check_domain_argument(arg_node: nodes.NodeNG, allow_iterable: bool = True) -> bool:
"""Ensure the argument node is a domain constant or variable.
We allow:
We currently only disallow `Platform.Xyz`
The plugin can be extended in the future (see #173374) to improve
consistency and only allow certain patterns for domain arguments,
such as:
- x.DOMAIN/x.domain attribute (including *_DOMAIN/*_domain)
- DOMAIN/domain name (including *_DOMAIN/*_domain)
- string literals
@@ -94,21 +99,8 @@ def _check_domain_argument(arg_node: nodes.NodeNG, allow_iterable: bool) -> bool
"""
match arg_node:
case nodes.Attribute():
if (
attrname := arg_node.attrname
) in _DOMAIN_CONSTANTS or attrname.endswith(_DOMAIN_SUFFIXES):
return True
case nodes.Const():
if isinstance(arg_node.value, str):
return True
case nodes.Name():
if (node_name := arg_node.name) in _DOMAIN_CONSTANTS or node_name.endswith(
_DOMAIN_SUFFIXES
):
return True
case nodes.Subscript():
# Ignore subscripts like dict["key"]
return True
if arg_node.expr.as_string() == "Platform":
return False
case nodes.Tuple():
if allow_iterable:
return all(
@@ -116,7 +108,7 @@ def _check_domain_argument(arg_node: nodes.NodeNG, allow_iterable: bool) -> bool
for element in arg_node.elts
)
return False
return True
class DomainConstantChecker(BaseChecker):
+1 -1
View File
@@ -74,7 +74,7 @@ dependencies = [
"typing-extensions>=4.15.0,<5.0",
"ulid-transform==2.2.9",
"urllib3>=2.0",
"uv==0.11.19",
"uv==0.11.21",
"voluptuous==0.15.2",
"voluptuous-serialize==2.7.0",
"voluptuous-openapi==0.3.0",
+1 -1
View File
@@ -55,7 +55,7 @@ standard-telnetlib==3.13.0
typing-extensions>=4.15.0,<5.0
ulid-transform==2.2.9
urllib3>=2.0
uv==0.11.19
uv==0.11.21
voluptuous-openapi==0.3.0
voluptuous-serialize==2.7.0
voluptuous==0.15.2
+25 -25
View File
@@ -178,7 +178,7 @@ aio-georss-gdacs==0.10
aio-ownet==0.0.5
# homeassistant.components.acaia
aioacaia==0.1.17
aioacaia==0.1.18
# homeassistant.components.airq
aioairq==0.4.7
@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
aioairzone==1.0.5
# homeassistant.components.alexa_devices
aioamazondevices==14.0.3
aioamazondevices==14.0.4
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -230,7 +230,7 @@ aiobotocore==2.21.1
aiocentriconnect==0.2.3
# homeassistant.components.comelit
aiocomelit==2.0.3
aiocomelit==2.0.5
# homeassistant.components.dhcp
aiodhcpwatcher==1.2.7
@@ -372,7 +372,7 @@ aiopnsense==1.0.8
aioptdevices==2026.03.2
# homeassistant.components.acmeda
aiopulse==0.4.6
aiopulse==0.4.7
# homeassistant.components.purpleair
aiopurpleair==2025.08.1
@@ -516,10 +516,10 @@ androidtvremote2==0.3.1
anel-pwrctrl-homeassistant==0.0.1.dev2
# homeassistant.components.anova
anova-wifi==0.17.0
anova-wifi==0.17.1
# homeassistant.components.anthemav
anthemav==1.4.1
anthemav==1.4.2
# homeassistant.components.anthropic
anthropic==0.108.0
@@ -657,7 +657,7 @@ beautifulsoup4==4.13.3
bizkaibus==0.1.1
# homeassistant.components.esphome
bleak-esphome==3.9.1
bleak-esphome==3.9.4
# homeassistant.components.bluetooth
bleak-retry-connector==4.6.1
@@ -669,13 +669,13 @@ bleak==3.0.2
blebox-uniapi==2.5.5
# homeassistant.components.blink
blinkpy==0.25.2
blinkpy==0.25.6
# homeassistant.components.bitcoin
blockchain==1.4.4
# homeassistant.components.blue_current
bluecurrent-api==1.3.2
bluecurrent-api==1.3.3
# homeassistant.components.bluemaestro
bluemaestro-ble==0.4.1
@@ -829,13 +829,13 @@ demetriek==1.3.0
denon-rs232==4.1.0
# homeassistant.components.denonavr
denonavr==1.3.2
denonavr==1.3.3
# homeassistant.components.devialet
devialet==1.5.7
# homeassistant.components.devolo_home_control
devolo-home-control-api==0.19.0
devolo-home-control-api==0.19.1
# homeassistant.components.devolo_home_network
devolo-plc-api==1.5.1
@@ -1081,7 +1081,7 @@ gcal-sync==8.0.0
genie-partner-sdk==1.0.11
# homeassistant.components.geniushub
geniushub-client==0.7.1
geniushub-client==0.7.4
# homeassistant.components.geocaching
geocachingapi==0.3.0
@@ -1522,7 +1522,7 @@ lupupy==0.3.2
lw12==0.9.2
# homeassistant.components.scrape
lxml==6.0.1
lxml==6.1.1
# homeassistant.components.matrix
matrix-nio==0.25.2
@@ -1948,7 +1948,7 @@ py-schluter==0.1.7
py-sucks==0.9.11
# homeassistant.components.synology_dsm
py-synologydsm-api==2.9.0
py-synologydsm-api==2.10.0
# homeassistant.components.unifi_access
py-unifi-access==1.3.0
@@ -2151,7 +2151,7 @@ pyegps==0.2.5
pyemoncms==0.1.3
# homeassistant.components.enphase_envoy
pyenphase==2.4.8
pyenphase==2.4.9
# homeassistant.components.envertech_evt800
pyenvertechevt800==0.2.4
@@ -2238,7 +2238,7 @@ pyialarm==2.2.0
pyicloud==2.4.1
# homeassistant.components.imou
pyimouapi==1.2.7
pyimouapi==1.2.8
# homeassistant.components.insteon
pyinsteon==1.6.4
@@ -2262,7 +2262,7 @@ pyiqvia==2022.04.0
pyirishrail==0.0.2
# homeassistant.components.iskra
pyiskra==0.1.27
pyiskra==0.1.29
# homeassistant.components.iss
pyiss==1.0.1
@@ -2495,10 +2495,10 @@ pyqwikswitch==0.93
pyrail==0.4.1
# homeassistant.components.rainbird
pyrainbird==6.3.0
pyrainbird==6.3.1
# homeassistant.components.playstation_network
pyrate-limiter==4.2.0
pyrate-limiter==4.4.0
# homeassistant.components.recswitch
pyrecswitch==1.0.2
@@ -2684,7 +2684,7 @@ python-join-api==0.1.1
python-kasa[speedups]==0.10.2
# homeassistant.components.linkplay
python-linkplay==0.2.12
python-linkplay==0.2.14
# homeassistant.components.melcloud
python-melcloud==0.1.3
@@ -2902,7 +2902,7 @@ renault-api==0.5.12
renson-endura-delta==1.7.2
# homeassistant.components.reolink
reolink-aio==0.21.0
reolink-aio==0.21.1
# homeassistant.components.radio_frequency
rf-protocols==4.2.0
@@ -3046,7 +3046,7 @@ slixmpp==1.13.2
smart-meter-texas==0.5.5
# homeassistant.components.snapcast
snapcast==2.3.7
snapcast==2.3.8
# homeassistant.components.sonos
soco==0.31.1
@@ -3100,7 +3100,7 @@ starlink-grpc-core==1.2.5
statsd==3.2.1
# homeassistant.components.trane
steamloop==1.2.0
steamloop==1.2.1
# homeassistant.components.steam_online
steamodd==4.21
@@ -3255,7 +3255,7 @@ uasiren==0.0.1
uhooapi==1.2.8
# homeassistant.components.unifiprotect
uiprotect==13.1.1
uiprotect==13.1.2
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.6.1
@@ -3423,7 +3423,7 @@ yalesmartalarmclient==0.4.3
# homeassistant.components.august
# homeassistant.components.yale
# homeassistant.components.yalexs_ble
yalexs-ble==3.3.0
yalexs-ble==3.3.1
# homeassistant.components.august
# homeassistant.components.yale
+2 -2
View File
@@ -19,7 +19,7 @@ mock-open==1.4.0
mypy==2.1.0
prek==0.2.28
pydantic==2.13.4
pylint==4.0.5
pylint==4.0.6
pylint-per-file-ignores==3.2.1
pipdeptree==2.26.1
pytest-asyncio==1.4.0
@@ -37,7 +37,7 @@ pytest==9.0.3
requests==2.34.2
requests-mock==1.12.1
respx==0.23.1
syrupy==5.3.1
syrupy==5.3.2
tqdm==4.67.1
types-aiofiles==24.1.0.20250822
types-atomicwrites==1.4.5.1
+44
View File
@@ -1,6 +1,7 @@
"""Tests for calendar platform of Holiday integration."""
from datetime import datetime, timedelta
import logging
from freezegun.api import FrozenDateTimeFactory
from holidays import CATHOLIC
@@ -17,6 +18,7 @@ from homeassistant.components.holiday.const import (
)
from homeassistant.const import CONF_COUNTRY
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
@@ -431,3 +433,45 @@ async def test_categories(
]
}
}
async def test_no_update_when_disabled(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
entity_registry: er.EntityRegistry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that a disabled calendar entity does not trigger updates."""
zone = await dt_util.async_get_time_zone("US/Hawaii")
freezer.move_to(datetime(2023, 1, 1, 0, 1, 1, tzinfo=zone))
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_COUNTRY: "US", CONF_PROVINCE: "AK"},
title="United States, AK",
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
await async_setup_component(hass, "calendar", {})
await hass.async_block_till_done()
entity_id = "calendar.united_states_ak"
state = hass.states.get(entity_id)
assert state is not None
entity_registry.async_update_entity(
entity_id, disabled_by=er.RegistryEntryDisabler.USER
)
await hass.async_block_till_done()
with caplog.at_level(logging.WARNING, logger="homeassistant.helpers.entity"):
freezer.move_to(datetime(2023, 1, 2, 0, 1, 1, tzinfo=zone))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (
"incorrectly being triggered for updates while it is disabled"
not in caplog.text
)
@@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(
test_devices=None, test_groups=None
)
assert len(mock_hap.hmip_device_by_entity_id) == 384
assert len(mock_hap.hmip_device_by_entity_id) == 385
async def test_hmip_remove_device(
@@ -1,6 +1,6 @@
"""Tests for HomematicIP Cloud sensor."""
from homematicip.base.enums import ValveState
from homematicip.base.enums import ValveState, WindowState
from homeassistant.components.homematicip_cloud import DOMAIN
from homeassistant.components.homematicip_cloud.entity import (
@@ -745,6 +745,38 @@ async def test_hmip_tilt_vibration_sensor_tilt_state(
assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_SECOND_TRIGGER_ANGLE] == 75
async def test_hmip_rotary_handle_window_state_sensor(
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
) -> None:
"""Test HomematicipWindowStateSensor exposes the three-way state of HmIP-SRH."""
entity_id = "sensor.fenstergriffsensor_window_state"
entity_name = "Fenstergriffsensor Window state"
device_model = "HmIP-SRH"
mock_hap = await default_mock_hap_factory.async_get_mock_hap(
test_devices=["Fenstergriffsensor"]
)
ha_state, hmip_device = get_and_check_entity_basics(
hass, mock_hap, entity_id, entity_name, device_model
)
assert ha_state.state == "tilted"
await async_manipulate_test_data(hass, hmip_device, "windowState", WindowState.OPEN)
ha_state = hass.states.get(entity_id)
assert ha_state.state == "open"
await async_manipulate_test_data(
hass, hmip_device, "windowState", WindowState.CLOSED
)
ha_state = hass.states.get(entity_id)
assert ha_state.state == "closed"
await async_manipulate_test_data(hass, hmip_device, "windowState", None)
ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_UNKNOWN
async def test_hmip_tilt_vibration_sensor_tilt_angle(
hass: HomeAssistant, default_mock_hap_factory: HomeFactory
) -> None:
@@ -0,0 +1,36 @@
# serializer version: 1
# name: test_device_registry
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'e0:da:dc:0a:12:34',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'jvc_projector',
'e0:da:dc:0a:12:34',
),
}),
'labels': set({
}),
'manufacturer': 'JVC',
'model': 'B2A2',
'model_id': None,
'name': 'JVC Projector',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
})
# ---
@@ -3,6 +3,7 @@
from unittest.mock import AsyncMock, patch
from jvcprojector import JvcProjectorAuthError, JvcProjectorTimeoutError
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.jvc_projector.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
@@ -30,6 +31,20 @@ async def test_init(
assert device.identifiers == {(DOMAIN, mac)}
async def test_device_registry(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_device: AsyncMock,
mock_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the device registry entry, including the network MAC connection."""
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, format_mac(MOCK_MAC))}
)
assert device_entry == snapshot
async def test_unload_config_entry(
hass: HomeAssistant,
mock_device: AsyncMock,
+26
View File
@@ -113,6 +113,32 @@ async def test_fan_set_speed(
)
async def test_fan_set_percentage_no_op_when_already_at_target(
hass: HomeAssistant,
mock_miele_client: MagicMock,
setup_platform: MockConfigEntry,
) -> None:
"""Test that set_percentage is a no-op when already at the target step."""
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_PERCENTAGE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PERCENTAGE: 50},
blocking=True,
)
mock_miele_client.send_action.assert_called_once_with(
"DummyAppliance_18", {"ventilationStep": 2}
)
mock_miele_client.send_action.reset_mock()
await hass.services.async_call(
FAN_DOMAIN,
SERVICE_SET_PERCENTAGE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PERCENTAGE: 50},
blocking=True,
)
mock_miele_client.send_action.assert_not_called()
async def test_fan_turn_on_w_percentage(
hass: HomeAssistant,
mock_miele_client: MagicMock,
+11 -16
View File
@@ -28,7 +28,6 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import init_integration
@@ -183,6 +182,7 @@ async def test_set_percentage(
async def test_fan_error(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the Modern Forms fans."""
@@ -191,11 +191,8 @@ async def test_fan_error(
aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=400)
with (
patch(
"homeassistant.components.modern_forms.coordinator.ModernFormsDevice.update"
),
pytest.raises(HomeAssistantError) as exc_info,
with patch(
"homeassistant.components.modern_forms.coordinator.ModernFormsDevice.update"
):
await hass.services.async_call(
FAN_DOMAIN,
@@ -203,15 +200,16 @@ async def test_fan_error(
{ATTR_ENTITY_ID: "fan.modernformsfan_fan"},
blocking=True,
)
assert exc_info.value.translation_domain == DOMAIN
assert exc_info.value.translation_key == "invalid_response"
await hass.async_block_till_done()
state = hass.states.get("fan.modernformsfan_fan")
assert state.state == STATE_ON
assert "Invalid response from API" in caplog.text
async def test_fan_connection_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test error handling of the Modern Forms fans."""
"""Test error handling of the Moder Forms fans."""
await init_integration(hass, aioclient_mock)
with (
@@ -222,7 +220,6 @@ async def test_fan_connection_error(
"homeassistant.components.modern_forms.coordinator.ModernFormsDevice.fan",
side_effect=ModernFormsConnectionError,
),
pytest.raises(HomeAssistantError) as exc_info,
):
await hass.services.async_call(
FAN_DOMAIN,
@@ -230,9 +227,7 @@ async def test_fan_connection_error(
{ATTR_ENTITY_ID: "fan.modernformsfan_fan"},
blocking=True,
)
await hass.async_block_till_done()
assert exc_info.value.translation_domain == DOMAIN
assert exc_info.value.translation_key == "communication_error"
state = hass.states.get("fan.modernformsfan_fan")
assert state.state == STATE_UNAVAILABLE
state = hass.states.get("fan.modernformsfan_fan")
assert state.state == STATE_UNAVAILABLE
+11 -16
View File
@@ -21,7 +21,6 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import init_integration
@@ -111,6 +110,7 @@ async def test_sleep_timer_services(
async def test_light_error(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the Modern Forms lights."""
@@ -119,11 +119,8 @@ async def test_light_error(
aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=400)
with (
patch(
"homeassistant.components.modern_forms.coordinator.ModernFormsDevice.update"
),
pytest.raises(HomeAssistantError) as exc_info,
with patch(
"homeassistant.components.modern_forms.coordinator.ModernFormsDevice.update"
):
await hass.services.async_call(
LIGHT_DOMAIN,
@@ -131,15 +128,16 @@ async def test_light_error(
{ATTR_ENTITY_ID: "light.modernformsfan_light"},
blocking=True,
)
assert exc_info.value.translation_domain == DOMAIN
assert exc_info.value.translation_key == "invalid_response"
await hass.async_block_till_done()
state = hass.states.get("light.modernformsfan_light")
assert state.state == STATE_ON
assert "Invalid response from API" in caplog.text
async def test_light_connection_error(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test error handling of the Modern Forms lights."""
"""Test error handling of the Moder Forms lights."""
await init_integration(hass, aioclient_mock)
with (
@@ -150,7 +148,6 @@ async def test_light_connection_error(
"homeassistant.components.modern_forms.coordinator.ModernFormsDevice.light",
side_effect=ModernFormsConnectionError,
),
pytest.raises(HomeAssistantError) as exc_info,
):
await hass.services.async_call(
LIGHT_DOMAIN,
@@ -158,9 +155,7 @@ async def test_light_connection_error(
{ATTR_ENTITY_ID: "light.modernformsfan_light"},
blocking=True,
)
await hass.async_block_till_done()
assert exc_info.value.translation_domain == DOMAIN
assert exc_info.value.translation_key == "communication_error"
state = hass.states.get("light.modernformsfan_light")
assert state.state == STATE_UNAVAILABLE
state = hass.states.get("light.modernformsfan_light")
assert state.state == STATE_UNAVAILABLE
+10 -15
View File
@@ -5,7 +5,6 @@ from unittest.mock import patch
from aiomodernforms import ModernFormsConnectionError
import pytest
from homeassistant.components.modern_forms.const import DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -15,7 +14,6 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import init_integration
@@ -104,6 +102,7 @@ async def test_switch_change_state(
async def test_switch_error(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the Modern Forms switches."""
await init_integration(hass, aioclient_mock)
@@ -111,11 +110,8 @@ async def test_switch_error(
aioclient_mock.clear_requests()
aioclient_mock.post("http://192.168.1.123:80/mf", text="", status=400)
with (
patch(
"homeassistant.components.modern_forms.coordinator.ModernFormsDevice.update"
),
pytest.raises(HomeAssistantError) as exc_info,
with patch(
"homeassistant.components.modern_forms.coordinator.ModernFormsDevice.update"
):
await hass.services.async_call(
SWITCH_DOMAIN,
@@ -123,9 +119,11 @@ async def test_switch_error(
{ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"},
blocking=True,
)
await hass.async_block_till_done()
assert exc_info.value.translation_domain == DOMAIN
assert exc_info.value.translation_key == "invalid_response"
state = hass.states.get("switch.modernformsfan_away_mode")
assert state.state == STATE_OFF
assert "Invalid response from API" in caplog.text
async def test_switch_connection_error(
@@ -142,7 +140,6 @@ async def test_switch_connection_error(
"homeassistant.components.modern_forms.coordinator.ModernFormsDevice.away",
side_effect=ModernFormsConnectionError,
),
pytest.raises(HomeAssistantError) as exc_info,
):
await hass.services.async_call(
SWITCH_DOMAIN,
@@ -150,9 +147,7 @@ async def test_switch_connection_error(
{ATTR_ENTITY_ID: "switch.modernformsfan_away_mode"},
blocking=True,
)
await hass.async_block_till_done()
assert exc_info.value.translation_domain == DOMAIN
assert exc_info.value.translation_key == "communication_error"
state = hass.states.get("switch.modernformsfan_away_mode")
assert state.state == STATE_UNAVAILABLE
state = hass.states.get("switch.modernformsfan_away_mode")
assert state.state == STATE_UNAVAILABLE
@@ -0,0 +1,36 @@
# serializer version: 1
# name: test_device_registry_bulb
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
tuple(
'mac',
'60:01:94:03:76:eb',
),
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'mystrom',
'6001940376EB',
),
}),
'labels': set({
}),
'manufacturer': 'myStrom',
'model': None,
'model_id': None,
'name': 'myStrom Device',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': '2.58.0',
'via_device_id': None,
})
# ---
+15
View File
@@ -4,10 +4,12 @@ from unittest.mock import AsyncMock, PropertyMock, patch
from pymystrom.exceptions import MyStromConnectionError
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.mystrom.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import (
MyStromBulbMock,
@@ -68,6 +70,19 @@ async def test_init_pir_and_unload(
assert config_entry.state is ConfigEntryState.NOT_LOADED
async def test_device_registry_bulb(
hass: HomeAssistant,
config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the bulb device registry entry, including the network MAC connection."""
await init_integration(hass, config_entry, 102)
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_MAC)})
assert device_entry == snapshot
async def test_init_switch_and_unload(
hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
+50
View File
@@ -5,6 +5,25 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from homeassistant.components.nasweb.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from tests.common import MockConfigEntry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "1.1.1.1",
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
unique_id=TEST_SERIAL_NUMBER,
)
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
@@ -59,3 +78,34 @@ def validate_input_all_ok() -> Generator[dict[str, AsyncMock | MagicMock]]:
BASE_NASWEB_DATA
+ "NotificationCoordinator.check_connection": check_status_confirmation,
}
@pytest.fixture
def mock_webio_api() -> Generator[MagicMock]:
"""Return a mocked WebioAPI client."""
with (
patch(
"homeassistant.components.nasweb.WebioAPI",
autospec=True,
) as webio_api_mock,
patch(
BASE_NASWEB_DATA + "NASwebData.get_webhook_url",
return_value="http://127.0.0.1:8123/api/webhook/test",
),
patch(
BASE_NASWEB_DATA + "NotificationCoordinator.check_connection",
return_value=True,
),
):
webio_api = webio_api_mock.return_value
webio_api.check_connection.return_value = True
webio_api.refresh_device_info.return_value = True
webio_api.get_serial_number.return_value = TEST_SERIAL_NUMBER
webio_api.get_name.return_value = "TestNASweb"
webio_api.status_subscription.return_value = True
webio_api.outputs = {}
webio_api.inputs = {}
webio_api.temp_sensor = {}
webio_api.thermostat = {}
webio_api.zones = {}
yield webio_api
+121
View File
@@ -0,0 +1,121 @@
"""Tests for the NASweb integration."""
from unittest.mock import MagicMock, patch
import pytest
from webio_api.api_client import AuthError
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers.network import NoURLAvailableError
from .conftest import BASE_NASWEB_DATA
from tests.common import MockConfigEntry
@pytest.mark.parametrize(
("mock_attr", "mock_value", "expected_translation_key"),
[
(
"refresh_device_info",
False,
"config_entry_error_internal_error",
),
(
"get_serial_number",
None,
"config_entry_error_internal_error",
),
(
"get_serial_number",
"different_serial",
"config_entry_error_serial_mismatch",
),
(
"status_subscription",
False,
"config_entry_error_internal_error",
),
],
)
async def test_setup_entry_config_entry_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_webio_api: MagicMock,
mock_attr: str,
mock_value: object,
expected_translation_key: str,
) -> None:
"""Test setup entry raises ConfigEntryError with correct translation key."""
getattr(mock_webio_api, mock_attr).return_value = mock_value
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
assert mock_config_entry.error_reason_translation_key == expected_translation_key
async def test_setup_entry_auth_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_webio_api: MagicMock,
) -> None:
"""Test setup entry raises ConfigEntryError on authentication failure."""
mock_webio_api.check_connection.side_effect = AuthError
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
assert (
mock_config_entry.error_reason_translation_key
== "config_entry_error_invalid_authentication"
)
async def test_setup_entry_no_status_update(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_webio_api: MagicMock,
) -> None:
"""Test setup entry raises ConfigEntryError when no status update received."""
with patch(
BASE_NASWEB_DATA + "NotificationCoordinator.check_connection",
return_value=False,
):
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
assert (
mock_config_entry.error_reason_translation_key
== "config_entry_error_no_status_update"
)
async def test_setup_entry_missing_internal_url(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_webio_api: MagicMock,
) -> None:
"""Test setup entry raises ConfigEntryError when internal URL is missing."""
with patch(
BASE_NASWEB_DATA + "NASwebData.get_webhook_url",
side_effect=NoURLAvailableError,
):
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
assert (
mock_config_entry.error_reason_translation_key
== "config_entry_error_missing_internal_url"
)
@@ -1728,3 +1728,22 @@ async def test_returns_array_friends(
assert response_json[0]["lat"] == 10
assert response_json[0]["lon"] == 20
assert response_json[0]["tid"] == "p1"
@pytest.mark.usefixtures("context")
async def test_mobile_beacon_uppercase_name(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that a mobile beacon with uppercase name gets a valid entity ID."""
await send_message(hass, LOCATION_TOPIC, LOCATION_MESSAGE)
message = build_message(
{"desc": "Office", "event": "enter"}, DEFAULT_BEACON_TRANSITION_MESSAGE
)
await send_message(hass, EVENT_TOPIC, message)
state = hass.states.get("device_tracker.beacon_office")
assert state is not None
assert state.state == "outer"
assert "sets an invalid entity ID" not in caplog.text

Some files were not shown because too many files have changed in this diff Show More