mirror of
https://github.com/home-assistant/core.git
synced 2026-06-15 05:32:40 +02:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c576d5267a | |||
| 1241a11c9d | |||
| 8f7447de58 | |||
| 1e54dba835 | |||
| 20583d6d1b | |||
| b94370ee51 | |||
| d068a2aa11 | |||
| 5d53a1f204 | |||
| 2908f37130 | |||
| a88a795ad3 | |||
| 73a36a2c47 | |||
| 054494181e | |||
| e4e8f901ab | |||
| b7a29bfa2f | |||
| 26b0079945 | |||
| 7454f40dd8 | |||
| 26b7d1e32c | |||
| f7342ea9b0 | |||
| 825d99ddaf | |||
| 401fae6bdd | |||
| 5433beeec1 | |||
| af60e248d3 | |||
| 8c452c280f | |||
| 3aec970321 | |||
| 687c91d5f4 | |||
| 377fdceb6c | |||
| 11a4533ccc | |||
| 52b2738b2a | |||
| 3fda722dbb | |||
| e01215da0e | |||
| 6c116cf3e4 | |||
| 8017e802dd | |||
| 501d956b1b | |||
| 8aca342a78 | |||
| bd68e9fbe3 | |||
| b75c839868 | |||
| 742bfb00ff | |||
| 987c19d991 | |||
| b4319c4d0c | |||
| 0fdb3ebed7 | |||
| efa3334616 | |||
| 9ec0f2fe4f | |||
| 9bc5e2b06b | |||
| 46a38cc481 | |||
| a63f2f1d20 | |||
| 744bb6a068 | |||
| d449e3e97b | |||
| 0df379704f | |||
| 4ab7ce04a8 | |||
| 210b08b637 | |||
| f0b448dc6e | |||
| b5a314bf60 | |||
| 741c342749 | |||
| f4d4df9c35 | |||
| bcbdf7b2bb | |||
| b3309ef169 | |||
| caaf5f9715 | |||
| 7ce7de3650 | |||
| 2c14c6be75 | |||
| e020f338ab | |||
| c85c2c4cd3 | |||
| c4e618e990 | |||
| 5efde60d21 | |||
| d9dc10ed81 |
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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%]"
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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,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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Generated
+1
-1
@@ -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
|
||||
|
||||
Generated
+25
-25
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
# ---
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user