mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
2024.1.3 (#107883)
This commit is contained in:
commit
99ee57aefc
@ -1550,7 +1550,7 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/zodiac/ @JulienTant
|
/tests/components/zodiac/ @JulienTant
|
||||||
/homeassistant/components/zone/ @home-assistant/core
|
/homeassistant/components/zone/ @home-assistant/core
|
||||||
/tests/components/zone/ @home-assistant/core
|
/tests/components/zone/ @home-assistant/core
|
||||||
/homeassistant/components/zoneminder/ @rohankapoorcom
|
/homeassistant/components/zoneminder/ @rohankapoorcom @nabbi
|
||||||
/homeassistant/components/zwave_js/ @home-assistant/z-wave
|
/homeassistant/components/zwave_js/ @home-assistant/z-wave
|
||||||
/tests/components/zwave_js/ @home-assistant/z-wave
|
/tests/components/zwave_js/ @home-assistant/z-wave
|
||||||
/homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS
|
/homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS
|
||||||
|
@ -15,6 +15,9 @@ from homeassistant.helpers import aiohttp_client
|
|||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from .const import STORAGE_ACCESS_TOKEN, STORAGE_REFRESH_TOKEN
|
||||||
|
from .diagnostics import async_redact_lwa_params
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token"
|
LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token"
|
||||||
@ -24,8 +27,6 @@ PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300
|
|||||||
STORAGE_KEY = "alexa_auth"
|
STORAGE_KEY = "alexa_auth"
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
STORAGE_EXPIRE_TIME = "expire_time"
|
STORAGE_EXPIRE_TIME = "expire_time"
|
||||||
STORAGE_ACCESS_TOKEN = "access_token"
|
|
||||||
STORAGE_REFRESH_TOKEN = "refresh_token"
|
|
||||||
|
|
||||||
|
|
||||||
class Auth:
|
class Auth:
|
||||||
@ -56,7 +57,7 @@ class Auth:
|
|||||||
}
|
}
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Calling LWA to get the access token (first time), with: %s",
|
"Calling LWA to get the access token (first time), with: %s",
|
||||||
json.dumps(lwa_params),
|
json.dumps(async_redact_lwa_params(lwa_params)),
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self._async_request_new_token(lwa_params)
|
return await self._async_request_new_token(lwa_params)
|
||||||
@ -133,7 +134,7 @@ class Auth:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
response_json = await response.json()
|
response_json = await response.json()
|
||||||
_LOGGER.debug("LWA response body : %s", response_json)
|
_LOGGER.debug("LWA response body : %s", async_redact_lwa_params(response_json))
|
||||||
|
|
||||||
access_token: str = response_json["access_token"]
|
access_token: str = response_json["access_token"]
|
||||||
refresh_token: str = response_json["refresh_token"]
|
refresh_token: str = response_json["refresh_token"]
|
||||||
|
@ -1112,13 +1112,17 @@ class AlexaThermostatController(AlexaCapability):
|
|||||||
"""Return what properties this entity supports."""
|
"""Return what properties this entity supports."""
|
||||||
properties = [{"name": "thermostatMode"}]
|
properties = [{"name": "thermostatMode"}]
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE:
|
if self.entity.domain == climate.DOMAIN:
|
||||||
|
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
||||||
|
properties.append({"name": "lowerSetpoint"})
|
||||||
|
properties.append({"name": "upperSetpoint"})
|
||||||
|
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE:
|
||||||
|
properties.append({"name": "targetSetpoint"})
|
||||||
|
elif (
|
||||||
|
self.entity.domain == water_heater.DOMAIN
|
||||||
|
and supported & water_heater.WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||||
|
):
|
||||||
properties.append({"name": "targetSetpoint"})
|
properties.append({"name": "targetSetpoint"})
|
||||||
if supported & water_heater.WaterHeaterEntityFeature.TARGET_TEMPERATURE:
|
|
||||||
properties.append({"name": "targetSetpoint"})
|
|
||||||
if supported & climate.ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
|
||||||
properties.append({"name": "lowerSetpoint"})
|
|
||||||
properties.append({"name": "upperSetpoint"})
|
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
def properties_proactively_reported(self) -> bool:
|
def properties_proactively_reported(self) -> bool:
|
||||||
|
@ -90,6 +90,9 @@ API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
|
|||||||
# we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode
|
# we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode
|
||||||
PRESET_MODE_NA = "-"
|
PRESET_MODE_NA = "-"
|
||||||
|
|
||||||
|
STORAGE_ACCESS_TOKEN = "access_token"
|
||||||
|
STORAGE_REFRESH_TOKEN = "refresh_token"
|
||||||
|
|
||||||
|
|
||||||
class Cause:
|
class Cause:
|
||||||
"""Possible causes for property changes.
|
"""Possible causes for property changes.
|
||||||
|
34
homeassistant/components/alexa/diagnostics.py
Normal file
34
homeassistant/components/alexa/diagnostics.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""Diagnostics helpers for Alexa."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
|
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
STORAGE_ACCESS_TOKEN = "access_token"
|
||||||
|
STORAGE_REFRESH_TOKEN = "refresh_token"
|
||||||
|
|
||||||
|
TO_REDACT_LWA = {
|
||||||
|
CONF_CLIENT_ID,
|
||||||
|
CONF_CLIENT_SECRET,
|
||||||
|
STORAGE_ACCESS_TOKEN,
|
||||||
|
STORAGE_REFRESH_TOKEN,
|
||||||
|
}
|
||||||
|
|
||||||
|
TO_REDACT_AUTH = {"correlationToken", "token"}
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_redact_lwa_params(lwa_params: dict[str, str]) -> dict[str, str]:
|
||||||
|
"""Redact lwa_params."""
|
||||||
|
return async_redact_data(lwa_params, TO_REDACT_LWA)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_redact_auth_data(mapping: Mapping[Any, Any]) -> dict[str, str]:
|
||||||
|
"""React auth data."""
|
||||||
|
return async_redact_data(mapping, TO_REDACT_AUTH)
|
@ -144,7 +144,6 @@ async def async_api_accept_grant(
|
|||||||
Async friendly.
|
Async friendly.
|
||||||
"""
|
"""
|
||||||
auth_code: str = directive.payload["grant"]["code"]
|
auth_code: str = directive.payload["grant"]["code"]
|
||||||
_LOGGER.debug("AcceptGrant code: %s", auth_code)
|
|
||||||
|
|
||||||
if config.supports_auth:
|
if config.supports_auth:
|
||||||
await config.async_accept_grant(auth_code)
|
await config.async_accept_grant(auth_code)
|
||||||
|
@ -25,6 +25,7 @@ from .const import (
|
|||||||
CONF_LOCALE,
|
CONF_LOCALE,
|
||||||
EVENT_ALEXA_SMART_HOME,
|
EVENT_ALEXA_SMART_HOME,
|
||||||
)
|
)
|
||||||
|
from .diagnostics import async_redact_auth_data
|
||||||
from .errors import AlexaBridgeUnreachableError, AlexaError
|
from .errors import AlexaBridgeUnreachableError, AlexaError
|
||||||
from .handlers import HANDLERS
|
from .handlers import HANDLERS
|
||||||
from .state_report import AlexaDirective
|
from .state_report import AlexaDirective
|
||||||
@ -149,12 +150,21 @@ class SmartHomeView(HomeAssistantView):
|
|||||||
user: User = request["hass_user"]
|
user: User = request["hass_user"]
|
||||||
message: dict[str, Any] = await request.json()
|
message: dict[str, Any] = await request.json()
|
||||||
|
|
||||||
_LOGGER.debug("Received Alexa Smart Home request: %s", message)
|
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Received Alexa Smart Home request: %s",
|
||||||
|
async_redact_auth_data(message),
|
||||||
|
)
|
||||||
|
|
||||||
response = await async_handle_message(
|
response = await async_handle_message(
|
||||||
hass, self.smart_home_config, message, context=core.Context(user_id=user.id)
|
hass, self.smart_home_config, message, context=core.Context(user_id=user.id)
|
||||||
)
|
)
|
||||||
_LOGGER.debug("Sending Alexa Smart Home response: %s", response)
|
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Sending Alexa Smart Home response: %s",
|
||||||
|
async_redact_auth_data(response),
|
||||||
|
)
|
||||||
|
|
||||||
return b"" if response is None else self.json(response)
|
return b"" if response is None else self.json(response)
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
Cause,
|
Cause,
|
||||||
)
|
)
|
||||||
|
from .diagnostics import async_redact_auth_data
|
||||||
from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
|
from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
|
||||||
from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink
|
from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink
|
||||||
|
|
||||||
@ -43,6 +44,8 @@ if TYPE_CHECKING:
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
|
|
||||||
|
TO_REDACT = {"correlationToken", "token"}
|
||||||
|
|
||||||
|
|
||||||
class AlexaDirective:
|
class AlexaDirective:
|
||||||
"""An incoming Alexa directive."""
|
"""An incoming Alexa directive."""
|
||||||
@ -379,7 +382,9 @@ async def async_send_changereport_message(
|
|||||||
response_text = await response.text()
|
response_text = await response.text()
|
||||||
|
|
||||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||||
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
|
_LOGGER.debug(
|
||||||
|
"Sent: %s", json.dumps(async_redact_auth_data(message_serialized))
|
||||||
|
)
|
||||||
_LOGGER.debug("Received (%s): %s", response.status, response_text)
|
_LOGGER.debug("Received (%s): %s", response.status, response_text)
|
||||||
|
|
||||||
if response.status == HTTPStatus.ACCEPTED:
|
if response.status == HTTPStatus.ACCEPTED:
|
||||||
@ -533,7 +538,9 @@ async def async_send_doorbell_event_message(
|
|||||||
response_text = await response.text()
|
response_text = await response.text()
|
||||||
|
|
||||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||||
_LOGGER.debug("Sent: %s", json.dumps(message_serialized))
|
_LOGGER.debug(
|
||||||
|
"Sent: %s", json.dumps(async_redact_auth_data(message_serialized))
|
||||||
|
)
|
||||||
_LOGGER.debug("Received (%s): %s", response.status, response_text)
|
_LOGGER.debug("Received (%s): %s", response.status, response_text)
|
||||||
|
|
||||||
if response.status == HTTPStatus.ACCEPTED:
|
if response.status == HTTPStatus.ACCEPTED:
|
||||||
|
39
homeassistant/components/aosmith/diagnostics.py
Normal file
39
homeassistant/components/aosmith/diagnostics.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""Diagnostics support for A. O. Smith."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import AOSmithData
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
TO_REDACT = {
|
||||||
|
"address",
|
||||||
|
"city",
|
||||||
|
"contactId",
|
||||||
|
"dsn",
|
||||||
|
"email",
|
||||||
|
"firstName",
|
||||||
|
"heaterSsid",
|
||||||
|
"id",
|
||||||
|
"lastName",
|
||||||
|
"phone",
|
||||||
|
"postalCode",
|
||||||
|
"registeredOwner",
|
||||||
|
"serial",
|
||||||
|
"ssid",
|
||||||
|
"state",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_config_entry_diagnostics(
|
||||||
|
hass: HomeAssistant, config_entry: ConfigEntry
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return diagnostics for a config entry."""
|
||||||
|
data: AOSmithData = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
|
all_device_info = await data.client.get_all_device_info()
|
||||||
|
return async_redact_data(all_device_info, TO_REDACT)
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["py-aosmith==1.0.1"]
|
"requirements": ["py-aosmith==1.0.4"]
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
SCAN_INTERVAL = 30
|
SCAN_INTERVAL = 300
|
||||||
|
|
||||||
|
|
||||||
class BlinkUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
class BlinkUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
|
@ -20,5 +20,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/blink",
|
"documentation": "https://www.home-assistant.io/integrations/blink",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["blinkpy"],
|
"loggers": ["blinkpy"],
|
||||||
"requirements": ["blinkpy==0.22.4"]
|
"requirements": ["blinkpy==0.22.5"]
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
"bleak==0.21.1",
|
"bleak==0.21.1",
|
||||||
"bleak-retry-connector==3.4.0",
|
"bleak-retry-connector==3.4.0",
|
||||||
"bluetooth-adapters==0.16.2",
|
"bluetooth-adapters==0.16.2",
|
||||||
"bluetooth-auto-recovery==1.2.3",
|
"bluetooth-auto-recovery==1.3.0",
|
||||||
"bluetooth-data-tools==1.19.0",
|
"bluetooth-data-tools==1.19.0",
|
||||||
"dbus-fast==2.21.0",
|
"dbus-fast==2.21.0",
|
||||||
"habluetooth==2.0.2"
|
"habluetooth==2.1.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -291,7 +291,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def _on_start() -> None:
|
async def _on_start() -> None:
|
||||||
"""Discover platforms."""
|
"""Handle cloud started after login."""
|
||||||
nonlocal loaded
|
nonlocal loaded
|
||||||
|
|
||||||
# Prevent multiple discovery
|
# Prevent multiple discovery
|
||||||
@ -299,14 +299,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
return
|
return
|
||||||
loaded = True
|
loaded = True
|
||||||
|
|
||||||
tts_info = {"platform_loaded": tts_platform_loaded}
|
|
||||||
|
|
||||||
await async_load_platform(hass, Platform.TTS, DOMAIN, tts_info, config)
|
|
||||||
await tts_platform_loaded.wait()
|
|
||||||
|
|
||||||
# The config entry should be loaded after the legacy tts platform is loaded
|
|
||||||
# to make sure that the tts integration is setup before we try to migrate
|
|
||||||
# old assist pipelines in the cloud stt entity.
|
|
||||||
await hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"})
|
await hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"})
|
||||||
|
|
||||||
async def _on_connect() -> None:
|
async def _on_connect() -> None:
|
||||||
@ -335,6 +327,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
account_link.async_setup(hass)
|
account_link.async_setup(hass)
|
||||||
|
|
||||||
|
hass.async_create_task(
|
||||||
|
async_load_platform(
|
||||||
|
hass,
|
||||||
|
Platform.TTS,
|
||||||
|
DOMAIN,
|
||||||
|
{"platform_loaded": tts_platform_loaded},
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async_call_later(
|
async_call_later(
|
||||||
hass=hass,
|
hass=hass,
|
||||||
delay=timedelta(hours=STARTUP_REPAIR_DELAY),
|
delay=timedelta(hours=STARTUP_REPAIR_DELAY),
|
||||||
|
@ -72,6 +72,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
_reauth_entry: ConfigEntry | None
|
_reauth_entry: ConfigEntry | None
|
||||||
_reauth_host: str
|
_reauth_host: str
|
||||||
_reauth_port: int
|
_reauth_port: int
|
||||||
|
_reauth_type: str
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
@ -109,6 +110,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
self._reauth_host = entry_data[CONF_HOST]
|
self._reauth_host = entry_data[CONF_HOST]
|
||||||
self._reauth_port = entry_data.get(CONF_PORT, DEFAULT_PORT)
|
self._reauth_port = entry_data.get(CONF_PORT, DEFAULT_PORT)
|
||||||
|
self._reauth_type = entry_data.get(CONF_TYPE, BRIDGE)
|
||||||
|
|
||||||
self.context["title_placeholders"] = {"host": self._reauth_host}
|
self.context["title_placeholders"] = {"host": self._reauth_host}
|
||||||
return await self.async_step_reauth_confirm()
|
return await self.async_step_reauth_confirm()
|
||||||
@ -127,6 +129,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
{
|
{
|
||||||
CONF_HOST: self._reauth_host,
|
CONF_HOST: self._reauth_host,
|
||||||
CONF_PORT: self._reauth_port,
|
CONF_PORT: self._reauth_port,
|
||||||
|
CONF_TYPE: self._reauth_type,
|
||||||
}
|
}
|
||||||
| user_input,
|
| user_input,
|
||||||
)
|
)
|
||||||
@ -144,6 +147,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_HOST: self._reauth_host,
|
CONF_HOST: self._reauth_host,
|
||||||
CONF_PORT: self._reauth_port,
|
CONF_PORT: self._reauth_port,
|
||||||
CONF_PIN: user_input[CONF_PIN],
|
CONF_PIN: user_input[CONF_PIN],
|
||||||
|
CONF_TYPE: self._reauth_type,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.hass.async_create_task(
|
self.hass.async_create_task(
|
||||||
|
@ -81,15 +81,11 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
try:
|
try:
|
||||||
await self.api.login()
|
await self.api.login()
|
||||||
return await self._async_update_system_data()
|
return await self._async_update_system_data()
|
||||||
except exceptions.CannotConnect as err:
|
except (exceptions.CannotConnect, exceptions.CannotRetrieveData) as err:
|
||||||
_LOGGER.warning("Connection error for %s", self._host)
|
raise UpdateFailed(repr(err)) from err
|
||||||
await self.api.close()
|
|
||||||
raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
|
|
||||||
except exceptions.CannotAuthenticate:
|
except exceptions.CannotAuthenticate:
|
||||||
raise ConfigEntryAuthFailed
|
raise ConfigEntryAuthFailed
|
||||||
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def _async_update_system_data(self) -> dict[str, Any]:
|
async def _async_update_system_data(self) -> dict[str, Any]:
|
||||||
"""Class method for updating data."""
|
"""Class method for updating data."""
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/comelit",
|
"documentation": "https://www.home-assistant.io/integrations/comelit",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aiocomelit"],
|
"loggers": ["aiocomelit"],
|
||||||
"requirements": ["aiocomelit==0.7.0"]
|
"requirements": ["aiocomelit==0.7.3"]
|
||||||
}
|
}
|
||||||
|
@ -481,7 +481,7 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
|||||||
def _get_toggle_function(
|
def _get_toggle_function(
|
||||||
self, fns: dict[str, Callable[_P, _R]]
|
self, fns: dict[str, Callable[_P, _R]]
|
||||||
) -> Callable[_P, _R]:
|
) -> Callable[_P, _R]:
|
||||||
if CoverEntityFeature.STOP | self.supported_features and (
|
if self.supported_features & CoverEntityFeature.STOP and (
|
||||||
self.is_closing or self.is_opening
|
self.is_closing or self.is_opening
|
||||||
):
|
):
|
||||||
return fns["stop"]
|
return fns["stop"]
|
||||||
|
@ -479,10 +479,20 @@ class EnvoyInverterEntity(EnvoySensorBaseEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> datetime.datetime | float:
|
def native_value(self) -> datetime.datetime | float | None:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
inverters = self.data.inverters
|
inverters = self.data.inverters
|
||||||
assert inverters is not None
|
assert inverters is not None
|
||||||
|
# Some envoy fw versions return an empty inverter array every 4 hours when
|
||||||
|
# no production is taking place. Prevent collection failure due to this
|
||||||
|
# as other data seems fine. Inverters will show unknown during this cycle.
|
||||||
|
if self._serial_number not in inverters:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Inverter %s not in returned inverters array (size: %s)",
|
||||||
|
self._serial_number,
|
||||||
|
len(inverters),
|
||||||
|
)
|
||||||
|
return None
|
||||||
return self.entity_description.value_fn(inverters[self._serial_number])
|
return self.entity_description.value_fn(inverters[self._serial_number])
|
||||||
|
|
||||||
|
|
||||||
|
@ -497,7 +497,6 @@ class EvoBroker:
|
|||||||
|
|
||||||
session_id = get_session_id(self.client_v1)
|
session_id = get_session_id(self.client_v1)
|
||||||
|
|
||||||
self.temps = {} # these are now stale, will fall back to v2 temps
|
|
||||||
try:
|
try:
|
||||||
temps = await self.client_v1.get_temperatures()
|
temps = await self.client_v1.get_temperatures()
|
||||||
|
|
||||||
@ -523,6 +522,11 @@ class EvoBroker:
|
|||||||
),
|
),
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
self.temps = {} # high-precision temps now considered stale
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.temps = {} # high-precision temps now considered stale
|
||||||
|
raise
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if str(self.client_v1.location_id) != self._location.locationId:
|
if str(self.client_v1.location_id) != self._location.locationId:
|
||||||
@ -654,6 +658,7 @@ class EvoChild(EvoDevice):
|
|||||||
assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check
|
assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check
|
||||||
|
|
||||||
if self._evo_broker.temps.get(self._evo_id) is not None:
|
if self._evo_broker.temps.get(self._evo_id) is not None:
|
||||||
|
# use high-precision temps if available
|
||||||
return self._evo_broker.temps[self._evo_id]
|
return self._evo_broker.temps[self._evo_id]
|
||||||
return self._evo_device.temperature
|
return self._evo_device.temperature
|
||||||
|
|
||||||
|
@ -118,7 +118,6 @@ class FAABinarySensor(CoordinatorEntity[FAADataUpdateCoordinator], BinarySensorE
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
_id = coordinator.data.code
|
_id = coordinator.data.code
|
||||||
self._attr_name = f"{_id} {description.name}"
|
|
||||||
self._attr_unique_id = f"{_id}_{description.key}"
|
self._attr_unique_id = f"{_id}_{description.key}"
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, _id)},
|
identifiers={(DOMAIN, _id)},
|
||||||
|
@ -27,6 +27,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
MAX_TEMP,
|
||||||
|
MIN_TEMP,
|
||||||
PRESET_TO_VENTILATION_MODE_MAP,
|
PRESET_TO_VENTILATION_MODE_MAP,
|
||||||
VENTILATION_TO_PRESET_MODE_MAP,
|
VENTILATION_TO_PRESET_MODE_MAP,
|
||||||
)
|
)
|
||||||
@ -67,6 +69,8 @@ class FlexitClimateEntity(ClimateEntity):
|
|||||||
|
|
||||||
_attr_target_temperature_step = PRECISION_HALVES
|
_attr_target_temperature_step = PRECISION_HALVES
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
|
_attr_max_temp = MAX_TEMP
|
||||||
|
_attr_min_temp = MIN_TEMP
|
||||||
|
|
||||||
def __init__(self, device: FlexitBACnet) -> None:
|
def __init__(self, device: FlexitBACnet) -> None:
|
||||||
"""Initialize the unit."""
|
"""Initialize the unit."""
|
||||||
|
@ -15,6 +15,9 @@ from homeassistant.components.climate import (
|
|||||||
|
|
||||||
DOMAIN = "flexit_bacnet"
|
DOMAIN = "flexit_bacnet"
|
||||||
|
|
||||||
|
MAX_TEMP = 30
|
||||||
|
MIN_TEMP = 10
|
||||||
|
|
||||||
VENTILATION_TO_PRESET_MODE_MAP = {
|
VENTILATION_TO_PRESET_MODE_MAP = {
|
||||||
VENTILATION_MODE_STOP: PRESET_NONE,
|
VENTILATION_MODE_STOP: PRESET_NONE,
|
||||||
VENTILATION_MODE_AWAY: PRESET_AWAY,
|
VENTILATION_MODE_AWAY: PRESET_AWAY,
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aio_geojson_generic_client"],
|
"loggers": ["aio_geojson_generic_client"],
|
||||||
"requirements": ["aio-geojson-generic-client==0.3"]
|
"requirements": ["aio-geojson-generic-client==0.4"]
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,18 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
language = lang
|
language = lang
|
||||||
break
|
break
|
||||||
|
if (
|
||||||
|
obj_holidays.supported_languages
|
||||||
|
and language not in obj_holidays.supported_languages
|
||||||
|
and (default_language := obj_holidays.default_language)
|
||||||
|
):
|
||||||
|
obj_holidays = country_holidays(
|
||||||
|
country,
|
||||||
|
subdiv=province,
|
||||||
|
years={dt_util.now().year, dt_util.now().year + 1},
|
||||||
|
language=default_language,
|
||||||
|
)
|
||||||
|
language = default_language
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
|
@ -118,7 +118,7 @@ async def async_setup_platform(
|
|||||||
|
|
||||||
mode = get_ip_mode(host)
|
mode = get_ip_mode(host)
|
||||||
mac = await hass.async_add_executor_job(partial(get_mac_address, **{mode: host}))
|
mac = await hass.async_add_executor_job(partial(get_mac_address, **{mode: host}))
|
||||||
if mac is None:
|
if mac is None or mac == "00:00:00:00:00:00":
|
||||||
raise PlatformNotReady("Cannot get the ip address of kef speaker.")
|
raise PlatformNotReady("Cannot get the ip address of kef speaker.")
|
||||||
|
|
||||||
unique_id = f"kef-{mac}"
|
unique_id = f"kef-{mac}"
|
||||||
|
@ -82,6 +82,9 @@ DATA_HASS_CONFIG: Final = "knx_hass_config"
|
|||||||
ATTR_COUNTER: Final = "counter"
|
ATTR_COUNTER: Final = "counter"
|
||||||
ATTR_SOURCE: Final = "source"
|
ATTR_SOURCE: Final = "source"
|
||||||
|
|
||||||
|
# dispatcher signal for KNX interface device triggers
|
||||||
|
SIGNAL_KNX_TELEGRAM_DICT: Final = "knx_telegram_dict"
|
||||||
|
|
||||||
AsyncMessageCallbackType = Callable[[Telegram], Awaitable[None]]
|
AsyncMessageCallbackType = Callable[[Telegram], Awaitable[None]]
|
||||||
MessageCallbackType = Callable[[Telegram], None]
|
MessageCallbackType = Callable[[Telegram], None]
|
||||||
|
|
||||||
|
@ -9,11 +9,12 @@ from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEM
|
|||||||
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
|
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
|
||||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||||
from homeassistant.helpers import selector
|
from homeassistant.helpers import selector
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from . import KNXModule
|
from . import KNXModule
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN, SIGNAL_KNX_TELEGRAM_DICT
|
||||||
from .project import KNXProject
|
from .project import KNXProject
|
||||||
from .schema import ga_list_validator
|
from .schema import ga_list_validator
|
||||||
from .telegrams import TelegramDict
|
from .telegrams import TelegramDict
|
||||||
@ -87,7 +88,6 @@ async def async_attach_trigger(
|
|||||||
trigger_data = trigger_info["trigger_data"]
|
trigger_data = trigger_info["trigger_data"]
|
||||||
dst_addresses: list[str] = config.get(EXTRA_FIELD_DESTINATION, [])
|
dst_addresses: list[str] = config.get(EXTRA_FIELD_DESTINATION, [])
|
||||||
job = HassJob(action, f"KNX device trigger {trigger_info}")
|
job = HassJob(action, f"KNX device trigger {trigger_info}")
|
||||||
knx: KNXModule = hass.data[DOMAIN]
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_call_trigger_action(telegram: TelegramDict) -> None:
|
def async_call_trigger_action(telegram: TelegramDict) -> None:
|
||||||
@ -99,6 +99,8 @@ async def async_attach_trigger(
|
|||||||
{"trigger": {**trigger_data, **telegram}},
|
{"trigger": {**trigger_data, **telegram}},
|
||||||
)
|
)
|
||||||
|
|
||||||
return knx.telegrams.async_listen_telegram(
|
return async_dispatcher_connect(
|
||||||
async_call_trigger_action, name="KNX device trigger call"
|
hass,
|
||||||
|
signal=SIGNAL_KNX_TELEGRAM_DICT,
|
||||||
|
target=async_call_trigger_action,
|
||||||
)
|
)
|
||||||
|
@ -11,10 +11,11 @@ from xknx.telegram import Telegram
|
|||||||
from xknx.telegram.apci import GroupValueResponse, GroupValueWrite
|
from xknx.telegram.apci import GroupValueResponse, GroupValueWrite
|
||||||
|
|
||||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN, SIGNAL_KNX_TELEGRAM_DICT
|
||||||
from .project import KNXProject
|
from .project import KNXProject
|
||||||
|
|
||||||
STORAGE_VERSION: Final = 1
|
STORAGE_VERSION: Final = 1
|
||||||
@ -87,6 +88,7 @@ class Telegrams:
|
|||||||
"""Handle incoming and outgoing telegrams from xknx."""
|
"""Handle incoming and outgoing telegrams from xknx."""
|
||||||
telegram_dict = self.telegram_to_dict(telegram)
|
telegram_dict = self.telegram_to_dict(telegram)
|
||||||
self.recent_telegrams.append(telegram_dict)
|
self.recent_telegrams.append(telegram_dict)
|
||||||
|
async_dispatcher_send(self.hass, SIGNAL_KNX_TELEGRAM_DICT, telegram_dict)
|
||||||
for job in self._jobs:
|
for job in self._jobs:
|
||||||
self.hass.async_run_hass_job(job, telegram_dict)
|
self.hass.async_run_hass_job(job, telegram_dict)
|
||||||
|
|
||||||
|
@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from bleak_retry_connector import BleakError, close_stale_connections, get_device
|
from bleak_retry_connector import (
|
||||||
|
BleakError,
|
||||||
|
close_stale_connections_by_address,
|
||||||
|
get_device,
|
||||||
|
)
|
||||||
from ld2410_ble import LD2410BLE
|
from ld2410_ble import LD2410BLE
|
||||||
|
|
||||||
from homeassistant.components import bluetooth
|
from homeassistant.components import bluetooth
|
||||||
@ -24,6 +28,9 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up LD2410 BLE from a config entry."""
|
"""Set up LD2410 BLE from a config entry."""
|
||||||
address: str = entry.data[CONF_ADDRESS]
|
address: str = entry.data[CONF_ADDRESS]
|
||||||
|
|
||||||
|
await close_stale_connections_by_address(address)
|
||||||
|
|
||||||
ble_device = bluetooth.async_ble_device_from_address(
|
ble_device = bluetooth.async_ble_device_from_address(
|
||||||
hass, address.upper(), True
|
hass, address.upper(), True
|
||||||
) or await get_device(address)
|
) or await get_device(address)
|
||||||
@ -32,8 +39,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
f"Could not find LD2410B device with address {address}"
|
f"Could not find LD2410B device with address {address}"
|
||||||
)
|
)
|
||||||
|
|
||||||
await close_stale_connections(ble_device)
|
|
||||||
|
|
||||||
ld2410_ble = LD2410BLE(ble_device)
|
ld2410_ble = LD2410BLE(ble_device)
|
||||||
|
|
||||||
coordinator = LD2410BLECoordinator(hass, ld2410_ble)
|
coordinator = LD2410BLECoordinator(hass, ld2410_ble)
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"station_id": "Sensor ID",
|
"sensor_id": "Sensor ID",
|
||||||
"show_on_map": "Show on map"
|
"show_on_map": "Show on map"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Coroutine
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp.hdrs import CONTENT_TYPE
|
from aiohttp.hdrs import CONTENT_TYPE
|
||||||
@ -267,11 +269,11 @@ class MicrosoftFace:
|
|||||||
"""Store group/person data and IDs."""
|
"""Store group/person data and IDs."""
|
||||||
return self._store
|
return self._store
|
||||||
|
|
||||||
async def update_store(self):
|
async def update_store(self) -> None:
|
||||||
"""Load all group/person data into local store."""
|
"""Load all group/person data into local store."""
|
||||||
groups = await self.call_api("get", "persongroups")
|
groups = await self.call_api("get", "persongroups")
|
||||||
|
|
||||||
remove_tasks = []
|
remove_tasks: list[Coroutine[Any, Any, None]] = []
|
||||||
new_entities = []
|
new_entities = []
|
||||||
for group in groups:
|
for group in groups:
|
||||||
g_id = group["personGroupId"]
|
g_id = group["personGroupId"]
|
||||||
@ -293,7 +295,7 @@ class MicrosoftFace:
|
|||||||
self._store[g_id][person["name"]] = person["personId"]
|
self._store[g_id][person["name"]] = person["personId"]
|
||||||
|
|
||||||
if remove_tasks:
|
if remove_tasks:
|
||||||
await asyncio.gather(remove_tasks)
|
await asyncio.gather(*remove_tasks)
|
||||||
await self._component.async_add_entities(new_entities)
|
await self._component.async_add_entities(new_entities)
|
||||||
|
|
||||||
async def call_api(self, method, function, data=None, binary=False, params=None):
|
async def call_api(self, method, function, data=None, binary=False, params=None):
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["dnspython", "mcstatus"],
|
"loggers": ["dnspython", "mcstatus"],
|
||||||
"quality_scale": "gold",
|
"quality_scale": "gold",
|
||||||
"requirements": ["mcstatus==11.0.0"]
|
"requirements": ["mcstatus==11.1.1"]
|
||||||
}
|
}
|
||||||
|
@ -70,8 +70,8 @@ MQTT_TEXT_ATTRIBUTES_BLOCKED = frozenset(
|
|||||||
|
|
||||||
def valid_text_size_configuration(config: ConfigType) -> ConfigType:
|
def valid_text_size_configuration(config: ConfigType) -> ConfigType:
|
||||||
"""Validate that the text length configuration is valid, throws if it isn't."""
|
"""Validate that the text length configuration is valid, throws if it isn't."""
|
||||||
if config[CONF_MIN] >= config[CONF_MAX]:
|
if config[CONF_MIN] > config[CONF_MAX]:
|
||||||
raise vol.Invalid("text length min must be >= max")
|
raise vol.Invalid("text length min must be <= max")
|
||||||
if config[CONF_MAX] > MAX_LENGTH_STATE_STATE:
|
if config[CONF_MAX] > MAX_LENGTH_STATE_STATE:
|
||||||
raise vol.Invalid(f"max text length must be <= {MAX_LENGTH_STATE_STATE}")
|
raise vol.Invalid(f"max text length must be <= {MAX_LENGTH_STATE_STATE}")
|
||||||
|
|
||||||
|
@ -18,5 +18,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["reolink_aio"],
|
"loggers": ["reolink_aio"],
|
||||||
"requirements": ["reolink-aio==0.8.5"]
|
"requirements": ["reolink-aio==0.8.6"]
|
||||||
}
|
}
|
||||||
|
@ -61,10 +61,7 @@ def async_load_screenlogic_services(hass: HomeAssistant):
|
|||||||
color_num,
|
color_num,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
if not await coordinator.gateway.async_set_color_lights(color_num):
|
await coordinator.gateway.async_set_color_lights(color_num)
|
||||||
raise HomeAssistantError(
|
|
||||||
f"Failed to call service '{SERVICE_SET_COLOR_MODE}'"
|
|
||||||
)
|
|
||||||
# Debounced refresh to catch any secondary
|
# Debounced refresh to catch any secondary
|
||||||
# changes in the device
|
# changes in the device
|
||||||
await coordinator.async_request_refresh()
|
await coordinator.async_request_refresh()
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aioshelly"],
|
"loggers": ["aioshelly"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aioshelly==7.0.0"],
|
"requirements": ["aioshelly==7.1.0"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_http._tcp.local.",
|
"type": "_http._tcp.local.",
|
||||||
|
@ -36,6 +36,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
from homeassistant.util.enum import try_parse_enum
|
||||||
|
|
||||||
from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS
|
from .const import CONF_SLEEP_PERIOD, SHAIR_MAX_WORK_HOURS
|
||||||
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
|
from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
|
||||||
@ -969,7 +970,7 @@ def _build_block_description(entry: RegistryEntry) -> BlockSensorDescription:
|
|||||||
name="",
|
name="",
|
||||||
icon=entry.original_icon,
|
icon=entry.original_icon,
|
||||||
native_unit_of_measurement=entry.unit_of_measurement,
|
native_unit_of_measurement=entry.unit_of_measurement,
|
||||||
device_class=entry.original_device_class,
|
device_class=try_parse_enum(SensorDeviceClass, entry.original_device_class),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,6 +61,10 @@ REPEAT_MODE_MAPPING_TO_SPOTIFY = {
|
|||||||
value: key for key, value in REPEAT_MODE_MAPPING_TO_HA.items()
|
value: key for key, value in REPEAT_MODE_MAPPING_TO_HA.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# This is a minimal representation of the DJ playlist that Spotify now offers
|
||||||
|
# The DJ is not fully integrated with the playlist API, so needs to have the playlist response mocked in order to maintain functionality
|
||||||
|
SPOTIFY_DJ_PLAYLIST = {"uri": "spotify:playlist:37i9dQZF1EYkqdzj48dyYq", "name": "DJ"}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -423,7 +427,19 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
|||||||
if context and (self._playlist is None or self._playlist["uri"] != uri):
|
if context and (self._playlist is None or self._playlist["uri"] != uri):
|
||||||
self._playlist = None
|
self._playlist = None
|
||||||
if context["type"] == MediaType.PLAYLIST:
|
if context["type"] == MediaType.PLAYLIST:
|
||||||
self._playlist = self.data.client.playlist(uri)
|
# The Spotify API does not currently support doing a lookup for the DJ playlist, so just use the minimal mock playlist object
|
||||||
|
if uri == SPOTIFY_DJ_PLAYLIST["uri"]:
|
||||||
|
self._playlist = SPOTIFY_DJ_PLAYLIST
|
||||||
|
else:
|
||||||
|
# Make sure any playlist lookups don't break the current playback state update
|
||||||
|
try:
|
||||||
|
self._playlist = self.data.client.playlist(uri)
|
||||||
|
except SpotifyException:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Unable to load spotify playlist '%s'. Continuing without playlist data",
|
||||||
|
uri,
|
||||||
|
)
|
||||||
|
self._playlist = None
|
||||||
|
|
||||||
device = self._currently_playing.get("device")
|
device = self._currently_playing.get("device")
|
||||||
if device is not None:
|
if device is not None:
|
||||||
|
@ -10,6 +10,7 @@ from opendata_transport.exceptions import (
|
|||||||
from homeassistant import config_entries, core
|
from homeassistant import config_entries, core
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import CONF_DESTINATION, CONF_START, DOMAIN
|
from .const import CONF_DESTINATION, CONF_START, DOMAIN
|
||||||
@ -65,3 +66,51 @@ async def async_unload_entry(
|
|||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(
|
||||||
|
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
|
||||||
|
) -> bool:
|
||||||
|
"""Migrate config entry."""
|
||||||
|
_LOGGER.debug("Migrating from version %s", config_entry.version)
|
||||||
|
|
||||||
|
if config_entry.minor_version > 3:
|
||||||
|
# This means the user has downgraded from a future version
|
||||||
|
return False
|
||||||
|
|
||||||
|
if config_entry.minor_version == 1:
|
||||||
|
# Remove wrongly registered devices and entries
|
||||||
|
new_unique_id = (
|
||||||
|
f"{config_entry.data[CONF_START]} {config_entry.data[CONF_DESTINATION]}"
|
||||||
|
)
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
device_entries = dr.async_entries_for_config_entry(
|
||||||
|
device_registry, config_entry_id=config_entry.entry_id
|
||||||
|
)
|
||||||
|
for dev in device_entries:
|
||||||
|
device_registry.async_remove_device(dev.id)
|
||||||
|
|
||||||
|
entity_id = entity_registry.async_get_entity_id(
|
||||||
|
Platform.SENSOR, DOMAIN, "None_departure"
|
||||||
|
)
|
||||||
|
if entity_id:
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
entity_id=entity_id,
|
||||||
|
new_unique_id=f"{new_unique_id}_departure",
|
||||||
|
)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Faulty entity with unique_id 'None_departure' migrated to new unique_id '%s'",
|
||||||
|
f"{new_unique_id}_departure",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set a valid unique id for config entries
|
||||||
|
config_entry.unique_id = new_unique_id
|
||||||
|
config_entry.minor_version = 2
|
||||||
|
hass.config_entries.async_update_entry(config_entry)
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Migration to minor version %s successful", config_entry.minor_version
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
@ -31,6 +31,7 @@ class SwissPublicTransportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"""Swiss public transport config flow."""
|
"""Swiss public transport config flow."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
MINOR_VERSION = 2
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
@ -59,6 +60,9 @@ class SwissPublicTransportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.exception("Unknown error")
|
_LOGGER.exception("Unknown error")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
else:
|
else:
|
||||||
|
await self.async_set_unique_id(
|
||||||
|
f"{user_input[CONF_START]} {user_input[CONF_DESTINATION]}"
|
||||||
|
)
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=f"{user_input[CONF_START]} {user_input[CONF_DESTINATION]}",
|
title=f"{user_input[CONF_START]} {user_input[CONF_DESTINATION]}",
|
||||||
data=user_input,
|
data=user_input,
|
||||||
@ -98,6 +102,9 @@ class SwissPublicTransportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
return self.async_abort(reason="unknown")
|
return self.async_abort(reason="unknown")
|
||||||
|
|
||||||
|
await self.async_set_unique_id(
|
||||||
|
f"{import_input[CONF_START]} {import_input[CONF_DESTINATION]}"
|
||||||
|
)
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=import_input[CONF_NAME],
|
title=import_input[CONF_NAME],
|
||||||
data=import_input,
|
data=import_input,
|
||||||
|
@ -122,15 +122,25 @@ class SwissPublicTransportSensor(
|
|||||||
entry_type=DeviceEntryType.SERVICE,
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Prepare the extra attributes at start."""
|
||||||
|
self._async_update_attrs()
|
||||||
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Handle the state update and prepare the extra state attributes."""
|
"""Handle the state update and prepare the extra state attributes."""
|
||||||
|
self._async_update_attrs()
|
||||||
|
return super()._handle_coordinator_update()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_update_attrs(self) -> None:
|
||||||
|
"""Update the extra state attributes based on the coordinator data."""
|
||||||
self._attr_extra_state_attributes = {
|
self._attr_extra_state_attributes = {
|
||||||
key: value
|
key: value
|
||||||
for key, value in self.coordinator.data.items()
|
for key, value in self.coordinator.data.items()
|
||||||
if key not in {"departure"}
|
if key not in {"departure"}
|
||||||
}
|
}
|
||||||
return super()._handle_coordinator_update()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> str:
|
def native_value(self) -> str:
|
||||||
|
@ -89,8 +89,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
# New device - create device
|
# New device - create device
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Discovered Switcher device - id: %s, name: %s, type: %s (%s)",
|
"Discovered Switcher device - id: %s, key: %s, name: %s, type: %s (%s)",
|
||||||
device.device_id,
|
device.device_id,
|
||||||
|
device.device_key,
|
||||||
device.name,
|
device.name,
|
||||||
device.device_type.value,
|
device.device_type.value,
|
||||||
device.device_type.hex_rep,
|
device.device_type.hex_rep,
|
||||||
|
@ -142,7 +142,9 @@ class SwitcherThermostatButtonEntity(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
async with SwitcherType2Api(
|
async with SwitcherType2Api(
|
||||||
self.coordinator.data.ip_address, self.coordinator.data.device_id
|
self.coordinator.data.ip_address,
|
||||||
|
self.coordinator.data.device_id,
|
||||||
|
self.coordinator.data.device_key,
|
||||||
) as swapi:
|
) as swapi:
|
||||||
response = await self.entity_description.press_fn(swapi, self._remote)
|
response = await self.entity_description.press_fn(swapi, self._remote)
|
||||||
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
||||||
|
@ -162,7 +162,9 @@ class SwitcherClimateEntity(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
async with SwitcherType2Api(
|
async with SwitcherType2Api(
|
||||||
self.coordinator.data.ip_address, self.coordinator.data.device_id
|
self.coordinator.data.ip_address,
|
||||||
|
self.coordinator.data.device_id,
|
||||||
|
self.coordinator.data.device_key,
|
||||||
) as swapi:
|
) as swapi:
|
||||||
response = await swapi.control_breeze_device(self._remote, **kwargs)
|
response = await swapi.control_breeze_device(self._remote, **kwargs)
|
||||||
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
||||||
|
@ -98,7 +98,9 @@ class SwitcherCoverEntity(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
async with SwitcherType2Api(
|
async with SwitcherType2Api(
|
||||||
self.coordinator.data.ip_address, self.coordinator.data.device_id
|
self.coordinator.data.ip_address,
|
||||||
|
self.coordinator.data.device_id,
|
||||||
|
self.coordinator.data.device_key,
|
||||||
) as swapi:
|
) as swapi:
|
||||||
response = await getattr(swapi, api)(*args)
|
response = await getattr(swapi, api)(*args)
|
||||||
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
||||||
|
@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
|
|
||||||
from .const import DATA_DEVICE, DOMAIN
|
from .const import DATA_DEVICE, DOMAIN
|
||||||
|
|
||||||
TO_REDACT = {"device_id", "ip_address", "mac_address"}
|
TO_REDACT = {"device_id", "device_key", "ip_address", "mac_address"}
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
async def async_get_config_entry_diagnostics(
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aioswitcher"],
|
"loggers": ["aioswitcher"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aioswitcher==3.3.0"]
|
"requirements": ["aioswitcher==3.4.1"]
|
||||||
}
|
}
|
||||||
|
@ -105,13 +105,17 @@ class SwitcherBaseSwitchEntity(
|
|||||||
|
|
||||||
async def _async_call_api(self, api: str, *args: Any) -> None:
|
async def _async_call_api(self, api: str, *args: Any) -> None:
|
||||||
"""Call Switcher API."""
|
"""Call Switcher API."""
|
||||||
_LOGGER.debug("Calling api for %s, api: '%s', args: %s", self.name, api, args)
|
_LOGGER.debug(
|
||||||
|
"Calling api for %s, api: '%s', args: %s", self.coordinator.name, api, args
|
||||||
|
)
|
||||||
response: SwitcherBaseResponse = None
|
response: SwitcherBaseResponse = None
|
||||||
error = None
|
error = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with SwitcherType1Api(
|
async with SwitcherType1Api(
|
||||||
self.coordinator.data.ip_address, self.coordinator.data.device_id
|
self.coordinator.data.ip_address,
|
||||||
|
self.coordinator.data.device_id,
|
||||||
|
self.coordinator.data.device_key,
|
||||||
) as swapi:
|
) as swapi:
|
||||||
response = await getattr(swapi, api)(*args)
|
response = await getattr(swapi, api)(*args)
|
||||||
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
||||||
|
@ -405,7 +405,7 @@ async def async_setup_entry(
|
|||||||
is_enabled = check_legacy_resource(
|
is_enabled = check_legacy_resource(
|
||||||
f"{_type}_{argument}", legacy_resources
|
f"{_type}_{argument}", legacy_resources
|
||||||
)
|
)
|
||||||
loaded_resources.add(f"{_type}_{argument}")
|
loaded_resources.add(f"{_type}_{slugify(argument)}")
|
||||||
entities.append(
|
entities.append(
|
||||||
SystemMonitorSensor(
|
SystemMonitorSensor(
|
||||||
sensor_registry,
|
sensor_registry,
|
||||||
@ -425,7 +425,7 @@ async def async_setup_entry(
|
|||||||
is_enabled = check_legacy_resource(
|
is_enabled = check_legacy_resource(
|
||||||
f"{_type}_{argument}", legacy_resources
|
f"{_type}_{argument}", legacy_resources
|
||||||
)
|
)
|
||||||
loaded_resources.add(f"{_type}_{argument}")
|
loaded_resources.add(f"{_type}_{slugify(argument)}")
|
||||||
entities.append(
|
entities.append(
|
||||||
SystemMonitorSensor(
|
SystemMonitorSensor(
|
||||||
sensor_registry,
|
sensor_registry,
|
||||||
@ -449,7 +449,7 @@ async def async_setup_entry(
|
|||||||
sensor_registry[(_type, argument)] = SensorData(
|
sensor_registry[(_type, argument)] = SensorData(
|
||||||
argument, None, None, None, None
|
argument, None, None, None, None
|
||||||
)
|
)
|
||||||
loaded_resources.add(f"{_type}_{argument}")
|
loaded_resources.add(f"{_type}_{slugify(argument)}")
|
||||||
entities.append(
|
entities.append(
|
||||||
SystemMonitorSensor(
|
SystemMonitorSensor(
|
||||||
sensor_registry,
|
sensor_registry,
|
||||||
@ -478,10 +478,13 @@ async def async_setup_entry(
|
|||||||
# of mount points automatically discovered
|
# of mount points automatically discovered
|
||||||
for resource in legacy_resources:
|
for resource in legacy_resources:
|
||||||
if resource.startswith("disk_"):
|
if resource.startswith("disk_"):
|
||||||
|
check_resource = slugify(resource)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Check resource %s already loaded in %s", resource, loaded_resources
|
"Check resource %s already loaded in %s",
|
||||||
|
check_resource,
|
||||||
|
loaded_resources,
|
||||||
)
|
)
|
||||||
if resource not in loaded_resources:
|
if check_resource not in loaded_resources:
|
||||||
split_index = resource.rfind("_")
|
split_index = resource.rfind("_")
|
||||||
_type = resource[:split_index]
|
_type = resource[:split_index]
|
||||||
argument = resource[split_index + 1 :]
|
argument = resource[split_index + 1 :]
|
||||||
|
@ -25,6 +25,11 @@ def get_all_disk_mounts() -> set[str]:
|
|||||||
"No permission for running user to access %s", part.mountpoint
|
"No permission for running user to access %s", part.mountpoint
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Mountpoint %s was excluded because of: %s", part.mountpoint, err
|
||||||
|
)
|
||||||
|
continue
|
||||||
if usage.total > 0 and part.device != "":
|
if usage.total > 0 and part.device != "":
|
||||||
disks.add(part.mountpoint)
|
disks.add(part.mountpoint)
|
||||||
_LOGGER.debug("Adding disks: %s", ", ".join(disks))
|
_LOGGER.debug("Adding disks: %s", ", ".join(disks))
|
||||||
|
@ -186,12 +186,13 @@ class TadoConnector:
|
|||||||
|
|
||||||
def get_mobile_devices(self):
|
def get_mobile_devices(self):
|
||||||
"""Return the Tado mobile devices."""
|
"""Return the Tado mobile devices."""
|
||||||
return self.tado.getMobileDevices()
|
return self.tado.get_mobile_devices()
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update the registered zones."""
|
"""Update the registered zones."""
|
||||||
self.update_devices()
|
self.update_devices()
|
||||||
|
self.update_mobile_devices()
|
||||||
self.update_zones()
|
self.update_zones()
|
||||||
self.update_home()
|
self.update_home()
|
||||||
|
|
||||||
@ -203,17 +204,31 @@ class TadoConnector:
|
|||||||
_LOGGER.error("Unable to connect to Tado while updating mobile devices")
|
_LOGGER.error("Unable to connect to Tado while updating mobile devices")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not mobile_devices:
|
||||||
|
_LOGGER.debug("No linked mobile devices found for home ID %s", self.home_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Errors are planned to be converted to exceptions
|
||||||
|
# in PyTado library, so this can be removed
|
||||||
|
if "errors" in mobile_devices and mobile_devices["errors"]:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Error for home ID %s while updating mobile devices: %s",
|
||||||
|
self.home_id,
|
||||||
|
mobile_devices["errors"],
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
for mobile_device in mobile_devices:
|
for mobile_device in mobile_devices:
|
||||||
self.data["mobile_device"][mobile_device["id"]] = mobile_device
|
self.data["mobile_device"][mobile_device["id"]] = mobile_device
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Dispatching update to %s mobile device: %s",
|
||||||
|
self.home_id,
|
||||||
|
mobile_device,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Dispatching update to %s mobile devices: %s",
|
|
||||||
self.home_id,
|
|
||||||
mobile_devices,
|
|
||||||
)
|
|
||||||
dispatcher_send(
|
dispatcher_send(
|
||||||
self.hass,
|
self.hass,
|
||||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED,
|
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(self.home_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_devices(self):
|
def update_devices(self):
|
||||||
@ -224,6 +239,20 @@ class TadoConnector:
|
|||||||
_LOGGER.error("Unable to connect to Tado while updating devices")
|
_LOGGER.error("Unable to connect to Tado while updating devices")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not devices:
|
||||||
|
_LOGGER.debug("No linked devices found for home ID %s", self.home_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Errors are planned to be converted to exceptions
|
||||||
|
# in PyTado library, so this can be removed
|
||||||
|
if "errors" in devices and devices["errors"]:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Error for home ID %s while updating devices: %s",
|
||||||
|
self.home_id,
|
||||||
|
devices["errors"],
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
for device in devices:
|
for device in devices:
|
||||||
device_short_serial_no = device["shortSerialNo"]
|
device_short_serial_no = device["shortSerialNo"]
|
||||||
_LOGGER.debug("Updating device %s", device_short_serial_no)
|
_LOGGER.debug("Updating device %s", device_short_serial_no)
|
||||||
|
@ -179,7 +179,7 @@ TADO_TO_HA_SWING_MODE_MAP = {
|
|||||||
DOMAIN = "tado"
|
DOMAIN = "tado"
|
||||||
|
|
||||||
SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}_{}"
|
SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}_{}"
|
||||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED = "tado_mobile_device_update_received"
|
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED = "tado_mobile_device_update_received_{}"
|
||||||
UNIQUE_ID = "unique_id"
|
UNIQUE_ID = "unique_id"
|
||||||
|
|
||||||
DEFAULT_NAME = "Tado"
|
DEFAULT_NAME = "Tado"
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -22,6 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from . import TadoConnector
|
||||||
from .const import CONF_HOME_ID, DATA, DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED
|
from .const import CONF_HOME_ID, DATA, DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -90,7 +90,7 @@ async def async_setup_entry(
|
|||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
hass,
|
hass,
|
||||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED,
|
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(tado.home_id),
|
||||||
update_devices,
|
update_devices,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -99,12 +99,12 @@ async def async_setup_entry(
|
|||||||
@callback
|
@callback
|
||||||
def add_tracked_entities(
|
def add_tracked_entities(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
tado: Any,
|
tado: TadoConnector,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
tracked: set[str],
|
tracked: set[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add new tracker entities from Tado."""
|
"""Add new tracker entities from Tado."""
|
||||||
_LOGGER.debug("Fetching Tado devices from API")
|
_LOGGER.debug("Fetching Tado devices from API for (newly) tracked entities")
|
||||||
new_tracked = []
|
new_tracked = []
|
||||||
for device_key, device in tado.data["mobile_device"].items():
|
for device_key, device in tado.data["mobile_device"].items():
|
||||||
if device_key in tracked:
|
if device_key in tracked:
|
||||||
@ -128,7 +128,7 @@ class TadoDeviceTrackerEntity(TrackerEntity):
|
|||||||
self,
|
self,
|
||||||
device_id: str,
|
device_id: str,
|
||||||
device_name: str,
|
device_name: str,
|
||||||
tado: Any,
|
tado: TadoConnector,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a Tado Device Tracker entity."""
|
"""Initialize a Tado Device Tracker entity."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -169,7 +169,7 @@ class TadoDeviceTrackerEntity(TrackerEntity):
|
|||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
self.hass,
|
self.hass,
|
||||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED,
|
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(self._tado.home_id),
|
||||||
self.on_demand_update,
|
self.on_demand_update,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -121,5 +121,6 @@ class TailwindDoorCoverEntity(TailwindDoorEntity, CoverEntity):
|
|||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="communication_error",
|
translation_key="communication_error",
|
||||||
) from exc
|
) from exc
|
||||||
self._attr_is_closing = False
|
finally:
|
||||||
|
self._attr_is_closing = False
|
||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
@ -220,6 +220,26 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
|||||||
hue, sat = tuple(int(val) for val in hs_color)
|
hue, sat = tuple(int(val) for val in hs_color)
|
||||||
await self.device.set_hsv(hue, sat, brightness, transition=transition)
|
await self.device.set_hsv(hue, sat, brightness, transition=transition)
|
||||||
|
|
||||||
|
async def _async_set_color_temp(
|
||||||
|
self, color_temp: float | int, brightness: int | None, transition: int | None
|
||||||
|
) -> None:
|
||||||
|
device = self.device
|
||||||
|
valid_temperature_range = device.valid_temperature_range
|
||||||
|
requested_color_temp = round(color_temp)
|
||||||
|
# Clamp color temp to valid range
|
||||||
|
# since if the light in a group we will
|
||||||
|
# get requests for color temps for the range
|
||||||
|
# of the group and not the light
|
||||||
|
clamped_color_temp = min(
|
||||||
|
valid_temperature_range.max,
|
||||||
|
max(valid_temperature_range.min, requested_color_temp),
|
||||||
|
)
|
||||||
|
await device.set_color_temp(
|
||||||
|
clamped_color_temp,
|
||||||
|
brightness=brightness,
|
||||||
|
transition=transition,
|
||||||
|
)
|
||||||
|
|
||||||
async def _async_turn_on_with_brightness(
|
async def _async_turn_on_with_brightness(
|
||||||
self, brightness: int | None, transition: int | None
|
self, brightness: int | None, transition: int | None
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -234,10 +254,8 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
|||||||
"""Turn the light on."""
|
"""Turn the light on."""
|
||||||
brightness, transition = self._async_extract_brightness_transition(**kwargs)
|
brightness, transition = self._async_extract_brightness_transition(**kwargs)
|
||||||
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||||
await self.device.set_color_temp(
|
await self._async_set_color_temp(
|
||||||
int(kwargs[ATTR_COLOR_TEMP_KELVIN]),
|
kwargs[ATTR_COLOR_TEMP_KELVIN], brightness, transition
|
||||||
brightness=brightness,
|
|
||||||
transition=transition,
|
|
||||||
)
|
)
|
||||||
if ATTR_HS_COLOR in kwargs:
|
if ATTR_HS_COLOR in kwargs:
|
||||||
await self._async_set_hsv(kwargs[ATTR_HS_COLOR], brightness, transition)
|
await self._async_set_hsv(kwargs[ATTR_HS_COLOR], brightness, transition)
|
||||||
@ -324,10 +342,8 @@ class TPLinkSmartLightStrip(TPLinkSmartBulb):
|
|||||||
# we have to set an HSV value to clear the effect
|
# we have to set an HSV value to clear the effect
|
||||||
# before we can set a color temp
|
# before we can set a color temp
|
||||||
await self.device.set_hsv(0, 0, brightness)
|
await self.device.set_hsv(0, 0, brightness)
|
||||||
await self.device.set_color_temp(
|
await self._async_set_color_temp(
|
||||||
int(kwargs[ATTR_COLOR_TEMP_KELVIN]),
|
kwargs[ATTR_COLOR_TEMP_KELVIN], brightness, transition
|
||||||
brightness=brightness,
|
|
||||||
transition=transition,
|
|
||||||
)
|
)
|
||||||
elif ATTR_HS_COLOR in kwargs:
|
elif ATTR_HS_COLOR in kwargs:
|
||||||
await self._async_set_hsv(kwargs[ATTR_HS_COLOR], brightness, transition)
|
await self._async_set_hsv(kwargs[ATTR_HS_COLOR], brightness, transition)
|
||||||
|
@ -94,7 +94,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
tasks = [_setup_lte(hass, conf) for conf in domain_config]
|
tasks = [_setup_lte(hass, conf) for conf in domain_config]
|
||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks)
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
for conf in domain_config:
|
for conf in domain_config:
|
||||||
for notify_conf in conf.get(CONF_NOTIFY, []):
|
for notify_conf in conf.get(CONF_NOTIFY, []):
|
||||||
|
@ -67,7 +67,6 @@ async def get_extended_options_schema(handler: SchemaCommonFlowHandler) -> vol.S
|
|||||||
CONF_MIN_GRADIENT, default=DEFAULT_MIN_GRADIENT
|
CONF_MIN_GRADIENT, default=DEFAULT_MIN_GRADIENT
|
||||||
): selector.NumberSelector(
|
): selector.NumberSelector(
|
||||||
selector.NumberSelectorConfig(
|
selector.NumberSelectorConfig(
|
||||||
min=0,
|
|
||||||
step="any",
|
step="any",
|
||||||
mode=selector.NumberSelectorMode.BOX,
|
mode=selector.NumberSelectorMode.BOX,
|
||||||
),
|
),
|
||||||
|
@ -173,15 +173,6 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
|||||||
ufp_value="is_vehicle_detection_on",
|
ufp_value="is_vehicle_detection_on",
|
||||||
ufp_perm=PermRequired.NO_WRITE,
|
ufp_perm=PermRequired.NO_WRITE,
|
||||||
),
|
),
|
||||||
ProtectBinaryEntityDescription(
|
|
||||||
key="smart_face",
|
|
||||||
name="Detections: Face",
|
|
||||||
icon="mdi:mdi-face",
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
ufp_required_field="can_detect_face",
|
|
||||||
ufp_value="is_face_detection_on",
|
|
||||||
ufp_perm=PermRequired.NO_WRITE,
|
|
||||||
),
|
|
||||||
ProtectBinaryEntityDescription(
|
ProtectBinaryEntityDescription(
|
||||||
key="smart_package",
|
key="smart_package",
|
||||||
name="Detections: Package",
|
name="Detections: Package",
|
||||||
@ -202,13 +193,22 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
ProtectBinaryEntityDescription(
|
ProtectBinaryEntityDescription(
|
||||||
key="smart_smoke",
|
key="smart_smoke",
|
||||||
name="Detections: Smoke/CO",
|
name="Detections: Smoke",
|
||||||
icon="mdi:fire",
|
icon="mdi:fire",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
ufp_required_field="can_detect_smoke",
|
ufp_required_field="can_detect_smoke",
|
||||||
ufp_value="is_smoke_detection_on",
|
ufp_value="is_smoke_detection_on",
|
||||||
ufp_perm=PermRequired.NO_WRITE,
|
ufp_perm=PermRequired.NO_WRITE,
|
||||||
),
|
),
|
||||||
|
ProtectBinaryEntityDescription(
|
||||||
|
key="smart_cmonx",
|
||||||
|
name="Detections: CO",
|
||||||
|
icon="mdi:molecule-co",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
ufp_required_field="can_detect_co",
|
||||||
|
ufp_value="is_co_detection_on",
|
||||||
|
ufp_perm=PermRequired.NO_WRITE,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||||
@ -342,7 +342,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
|||||||
key="motion",
|
key="motion",
|
||||||
name="Motion",
|
name="Motion",
|
||||||
device_class=BinarySensorDeviceClass.MOTION,
|
device_class=BinarySensorDeviceClass.MOTION,
|
||||||
ufp_value="is_motion_detected",
|
ufp_value="is_motion_currently_detected",
|
||||||
ufp_enabled="is_motion_detection_on",
|
ufp_enabled="is_motion_detection_on",
|
||||||
ufp_event_obj="last_motion_event",
|
ufp_event_obj="last_motion_event",
|
||||||
),
|
),
|
||||||
@ -350,7 +350,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
|||||||
key="smart_obj_any",
|
key="smart_obj_any",
|
||||||
name="Object Detected",
|
name="Object Detected",
|
||||||
icon="mdi:eye",
|
icon="mdi:eye",
|
||||||
ufp_value="is_smart_detected",
|
ufp_value="is_smart_currently_detected",
|
||||||
ufp_required_field="feature_flags.has_smart_detect",
|
ufp_required_field="feature_flags.has_smart_detect",
|
||||||
ufp_event_obj="last_smart_detect_event",
|
ufp_event_obj="last_smart_detect_event",
|
||||||
),
|
),
|
||||||
@ -358,7 +358,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
|||||||
key="smart_obj_person",
|
key="smart_obj_person",
|
||||||
name="Person Detected",
|
name="Person Detected",
|
||||||
icon="mdi:walk",
|
icon="mdi:walk",
|
||||||
ufp_value="is_smart_detected",
|
ufp_value="is_person_currently_detected",
|
||||||
ufp_required_field="can_detect_person",
|
ufp_required_field="can_detect_person",
|
||||||
ufp_enabled="is_person_detection_on",
|
ufp_enabled="is_person_detection_on",
|
||||||
ufp_event_obj="last_person_detect_event",
|
ufp_event_obj="last_person_detect_event",
|
||||||
@ -367,25 +367,16 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
|||||||
key="smart_obj_vehicle",
|
key="smart_obj_vehicle",
|
||||||
name="Vehicle Detected",
|
name="Vehicle Detected",
|
||||||
icon="mdi:car",
|
icon="mdi:car",
|
||||||
ufp_value="is_smart_detected",
|
ufp_value="is_vehicle_currently_detected",
|
||||||
ufp_required_field="can_detect_vehicle",
|
ufp_required_field="can_detect_vehicle",
|
||||||
ufp_enabled="is_vehicle_detection_on",
|
ufp_enabled="is_vehicle_detection_on",
|
||||||
ufp_event_obj="last_vehicle_detect_event",
|
ufp_event_obj="last_vehicle_detect_event",
|
||||||
),
|
),
|
||||||
ProtectBinaryEventEntityDescription(
|
|
||||||
key="smart_obj_face",
|
|
||||||
name="Face Detected",
|
|
||||||
icon="mdi:mdi-face",
|
|
||||||
ufp_value="is_smart_detected",
|
|
||||||
ufp_required_field="can_detect_face",
|
|
||||||
ufp_enabled="is_face_detection_on",
|
|
||||||
ufp_event_obj="last_face_detect_event",
|
|
||||||
),
|
|
||||||
ProtectBinaryEventEntityDescription(
|
ProtectBinaryEventEntityDescription(
|
||||||
key="smart_obj_package",
|
key="smart_obj_package",
|
||||||
name="Package Detected",
|
name="Package Detected",
|
||||||
icon="mdi:package-variant-closed",
|
icon="mdi:package-variant-closed",
|
||||||
ufp_value="is_smart_detected",
|
ufp_value="is_package_currently_detected",
|
||||||
ufp_required_field="can_detect_package",
|
ufp_required_field="can_detect_package",
|
||||||
ufp_enabled="is_package_detection_on",
|
ufp_enabled="is_package_detection_on",
|
||||||
ufp_event_obj="last_package_detect_event",
|
ufp_event_obj="last_package_detect_event",
|
||||||
@ -394,7 +385,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
|||||||
key="smart_audio_any",
|
key="smart_audio_any",
|
||||||
name="Audio Object Detected",
|
name="Audio Object Detected",
|
||||||
icon="mdi:eye",
|
icon="mdi:eye",
|
||||||
ufp_value="is_smart_detected",
|
ufp_value="is_audio_currently_detected",
|
||||||
ufp_required_field="feature_flags.has_smart_detect",
|
ufp_required_field="feature_flags.has_smart_detect",
|
||||||
ufp_event_obj="last_smart_audio_detect_event",
|
ufp_event_obj="last_smart_audio_detect_event",
|
||||||
),
|
),
|
||||||
@ -402,7 +393,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
|||||||
key="smart_audio_smoke",
|
key="smart_audio_smoke",
|
||||||
name="Smoke Alarm Detected",
|
name="Smoke Alarm Detected",
|
||||||
icon="mdi:fire",
|
icon="mdi:fire",
|
||||||
ufp_value="is_smart_detected",
|
ufp_value="is_smoke_currently_detected",
|
||||||
ufp_required_field="can_detect_smoke",
|
ufp_required_field="can_detect_smoke",
|
||||||
ufp_enabled="is_smoke_detection_on",
|
ufp_enabled="is_smoke_detection_on",
|
||||||
ufp_event_obj="last_smoke_detect_event",
|
ufp_event_obj="last_smoke_detect_event",
|
||||||
@ -410,10 +401,10 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
|||||||
ProtectBinaryEventEntityDescription(
|
ProtectBinaryEventEntityDescription(
|
||||||
key="smart_audio_cmonx",
|
key="smart_audio_cmonx",
|
||||||
name="CO Alarm Detected",
|
name="CO Alarm Detected",
|
||||||
icon="mdi:fire",
|
icon="mdi:molecule-co",
|
||||||
ufp_value="is_smart_detected",
|
ufp_value="is_cmonx_currently_detected",
|
||||||
ufp_required_field="can_detect_smoke",
|
ufp_required_field="can_detect_co",
|
||||||
ufp_enabled="is_smoke_detection_on",
|
ufp_enabled="is_co_detection_on",
|
||||||
ufp_event_obj="last_cmonx_detect_event",
|
ufp_event_obj="last_cmonx_detect_event",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -619,7 +610,7 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity):
|
|||||||
@callback
|
@callback
|
||||||
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
|
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
|
||||||
super()._async_update_device_from_protect(device)
|
super()._async_update_device_from_protect(device)
|
||||||
is_on = self.entity_description.get_is_on(self._event)
|
is_on = self.entity_description.get_is_on(self.device, self._event)
|
||||||
self._attr_is_on: bool | None = is_on
|
self._attr_is_on: bool | None = is_on
|
||||||
if not is_on:
|
if not is_on:
|
||||||
self._event = None
|
self._event = None
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyunifiprotect", "unifi_discovery"],
|
"loggers": ["pyunifiprotect", "unifi_discovery"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pyunifiprotect==4.22.5", "unifi-discovery==1.1.7"],
|
"requirements": ["pyunifiprotect==4.23.2", "unifi-discovery==1.1.7"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Ubiquiti Networks",
|
"manufacturer": "Ubiquiti Networks",
|
||||||
|
@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
|
|||||||
from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel
|
from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel
|
||||||
|
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
from homeassistant.util import dt as dt_util
|
|
||||||
|
|
||||||
from .utils import get_nested_attr
|
from .utils import get_nested_attr
|
||||||
|
|
||||||
@ -114,17 +113,10 @@ class ProtectEventMixin(ProtectRequiredKeysMixin[T]):
|
|||||||
return cast(Event, getattr(obj, self.ufp_event_obj, None))
|
return cast(Event, getattr(obj, self.ufp_event_obj, None))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_is_on(self, event: Event | None) -> bool:
|
def get_is_on(self, obj: T, event: Event | None) -> bool:
|
||||||
"""Return value if event is active."""
|
"""Return value if event is active."""
|
||||||
if event is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
now = dt_util.utcnow()
|
return event is not None and self.get_ufp_value(obj)
|
||||||
value = now > event.start
|
|
||||||
if value and event.end is not None and now > event.end:
|
|
||||||
value = False
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
@ -527,7 +527,7 @@ EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = (
|
|||||||
name="License Plate Detected",
|
name="License Plate Detected",
|
||||||
icon="mdi:car",
|
icon="mdi:car",
|
||||||
translation_key="license_plate",
|
translation_key="license_plate",
|
||||||
ufp_value="is_smart_detected",
|
ufp_value="is_license_plate_currently_detected",
|
||||||
ufp_required_field="can_detect_license_plate",
|
ufp_required_field="can_detect_license_plate",
|
||||||
ufp_event_obj="last_license_plate_detect_event",
|
ufp_event_obj="last_license_plate_detect_event",
|
||||||
),
|
),
|
||||||
@ -781,7 +781,7 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity):
|
|||||||
EventEntityMixin._async_update_device_from_protect(self, device)
|
EventEntityMixin._async_update_device_from_protect(self, device)
|
||||||
event = self._event
|
event = self._event
|
||||||
entity_description = self.entity_description
|
entity_description = self.entity_description
|
||||||
is_on = entity_description.get_is_on(event)
|
is_on = entity_description.get_is_on(self.device, self._event)
|
||||||
is_license_plate = (
|
is_license_plate = (
|
||||||
entity_description.ufp_event_obj == "last_license_plate_detect_event"
|
entity_description.ufp_event_obj == "last_license_plate_detect_event"
|
||||||
)
|
)
|
||||||
|
@ -135,6 +135,16 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
|||||||
ufp_set_method="set_osd_bitrate",
|
ufp_set_method="set_osd_bitrate",
|
||||||
ufp_perm=PermRequired.WRITE,
|
ufp_perm=PermRequired.WRITE,
|
||||||
),
|
),
|
||||||
|
ProtectSwitchEntityDescription(
|
||||||
|
key="color_night_vision",
|
||||||
|
name="Color Night Vision",
|
||||||
|
icon="mdi:light-flood-down",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
ufp_required_field="has_color_night_vision",
|
||||||
|
ufp_value="isp_settings.is_color_night_vision_enabled",
|
||||||
|
ufp_set_method="set_color_night_vision",
|
||||||
|
ufp_perm=PermRequired.WRITE,
|
||||||
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key="motion",
|
key="motion",
|
||||||
name="Detections: Motion",
|
name="Detections: Motion",
|
||||||
@ -167,17 +177,6 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
|||||||
ufp_set_method="set_vehicle_detection",
|
ufp_set_method="set_vehicle_detection",
|
||||||
ufp_perm=PermRequired.WRITE,
|
ufp_perm=PermRequired.WRITE,
|
||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
|
||||||
key="smart_face",
|
|
||||||
name="Detections: Face",
|
|
||||||
icon="mdi:human-greeting",
|
|
||||||
entity_category=EntityCategory.CONFIG,
|
|
||||||
ufp_required_field="can_detect_face",
|
|
||||||
ufp_value="is_face_detection_on",
|
|
||||||
ufp_enabled="is_recording_enabled",
|
|
||||||
ufp_set_method="set_face_detection",
|
|
||||||
ufp_perm=PermRequired.WRITE,
|
|
||||||
),
|
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key="smart_package",
|
key="smart_package",
|
||||||
name="Detections: Package",
|
name="Detections: Package",
|
||||||
@ -202,7 +201,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
|||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key="smart_smoke",
|
key="smart_smoke",
|
||||||
name="Detections: Smoke/CO",
|
name="Detections: Smoke",
|
||||||
icon="mdi:fire",
|
icon="mdi:fire",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_required_field="can_detect_smoke",
|
ufp_required_field="can_detect_smoke",
|
||||||
@ -212,13 +211,14 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
|||||||
ufp_perm=PermRequired.WRITE,
|
ufp_perm=PermRequired.WRITE,
|
||||||
),
|
),
|
||||||
ProtectSwitchEntityDescription(
|
ProtectSwitchEntityDescription(
|
||||||
key="color_night_vision",
|
key="smart_cmonx",
|
||||||
name="Color Night Vision",
|
name="Detections: CO",
|
||||||
icon="mdi:light-flood-down",
|
icon="mdi:molecule-co",
|
||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
ufp_required_field="has_color_night_vision",
|
ufp_required_field="can_detect_co",
|
||||||
ufp_value="isp_settings.is_color_night_vision_enabled",
|
ufp_value="is_co_detection_on",
|
||||||
ufp_set_method="set_color_night_vision",
|
ufp_enabled="is_recording_enabled",
|
||||||
|
ufp_set_method="set_cmonx_detection",
|
||||||
ufp_perm=PermRequired.WRITE,
|
ufp_perm=PermRequired.WRITE,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -186,9 +186,10 @@ class ValveEntity(Entity):
|
|||||||
|
|
||||||
@final
|
@final
|
||||||
@property
|
@property
|
||||||
def state_attributes(self) -> dict[str, Any]:
|
def state_attributes(self) -> dict[str, Any] | None:
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
|
if not self.reports_position:
|
||||||
|
return None
|
||||||
return {ATTR_CURRENT_POSITION: self.current_valve_position}
|
return {ATTR_CURRENT_POSITION: self.current_valve_position}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Support for ZoneMinder."""
|
"""Support for ZoneMinder."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from requests.exceptions import ConnectionError as RequestsConnectionError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from zoneminder.zm import ZoneMinder
|
from zoneminder.zm import ZoneMinder
|
||||||
|
|
||||||
@ -75,7 +76,14 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
)
|
)
|
||||||
hass.data[DOMAIN][host_name] = zm_client
|
hass.data[DOMAIN][host_name] = zm_client
|
||||||
|
|
||||||
success = zm_client.login() and success
|
try:
|
||||||
|
success = zm_client.login() and success
|
||||||
|
except RequestsConnectionError as ex:
|
||||||
|
_LOGGER.error(
|
||||||
|
"ZoneMinder connection failure to %s: %s",
|
||||||
|
host_name,
|
||||||
|
ex,
|
||||||
|
)
|
||||||
|
|
||||||
def set_active_state(call: ServiceCall) -> None:
|
def set_active_state(call: ServiceCall) -> None:
|
||||||
"""Set the ZoneMinder run state to the given state name."""
|
"""Set the ZoneMinder run state to the given state name."""
|
||||||
|
@ -8,6 +8,7 @@ from zoneminder.zm import ZoneMinder
|
|||||||
|
|
||||||
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
@ -28,8 +29,9 @@ def setup_platform(
|
|||||||
zm_client: ZoneMinder
|
zm_client: ZoneMinder
|
||||||
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
||||||
if not (monitors := zm_client.get_monitors()):
|
if not (monitors := zm_client.get_monitors()):
|
||||||
_LOGGER.warning("Could not fetch monitors from ZoneMinder host: %s")
|
raise PlatformNotReady(
|
||||||
return
|
"Camera could not fetch any monitors from ZoneMinder"
|
||||||
|
)
|
||||||
|
|
||||||
for monitor in monitors:
|
for monitor in monitors:
|
||||||
_LOGGER.info("Initializing camera %s", monitor.id)
|
_LOGGER.info("Initializing camera %s", monitor.id)
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"domain": "zoneminder",
|
"domain": "zoneminder",
|
||||||
"name": "ZoneMinder",
|
"name": "ZoneMinder",
|
||||||
"codeowners": ["@rohankapoorcom"],
|
"codeowners": ["@rohankapoorcom", "@nabbi"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/zoneminder",
|
"documentation": "https://www.home-assistant.io/integrations/zoneminder",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["zoneminder"],
|
"loggers": ["zoneminder"],
|
||||||
"requirements": ["zm-py==0.5.2"]
|
"requirements": ["zm-py==0.5.4"]
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
|
|||||||
)
|
)
|
||||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
@ -77,7 +78,9 @@ def setup_platform(
|
|||||||
zm_client: ZoneMinder
|
zm_client: ZoneMinder
|
||||||
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
||||||
if not (monitors := zm_client.get_monitors()):
|
if not (monitors := zm_client.get_monitors()):
|
||||||
_LOGGER.warning("Could not fetch any monitors from ZoneMinder")
|
raise PlatformNotReady(
|
||||||
|
"Sensor could not fetch any monitors from ZoneMinder"
|
||||||
|
)
|
||||||
|
|
||||||
for monitor in monitors:
|
for monitor in monitors:
|
||||||
sensors.append(ZMSensorMonitors(monitor))
|
sensors.append(ZMSensorMonitors(monitor))
|
||||||
|
@ -11,6 +11,7 @@ from zoneminder.zm import ZoneMinder
|
|||||||
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
|
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
|
||||||
from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON
|
from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
@ -42,8 +43,9 @@ def setup_platform(
|
|||||||
zm_client: ZoneMinder
|
zm_client: ZoneMinder
|
||||||
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
||||||
if not (monitors := zm_client.get_monitors()):
|
if not (monitors := zm_client.get_monitors()):
|
||||||
_LOGGER.warning("Could not fetch monitors from ZoneMinder")
|
raise PlatformNotReady(
|
||||||
return
|
"Switch could not fetch any monitors from ZoneMinder"
|
||||||
|
)
|
||||||
|
|
||||||
for monitor in monitors:
|
for monitor in monitors:
|
||||||
switches.append(ZMSwitchMonitors(monitor, on_state, off_state))
|
switches.append(ZMSwitchMonitors(monitor, on_state, off_state))
|
||||||
|
@ -16,7 +16,7 @@ from .helpers.deprecation import (
|
|||||||
APPLICATION_NAME: Final = "HomeAssistant"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2024
|
MAJOR_VERSION: Final = 2024
|
||||||
MINOR_VERSION: Final = 1
|
MINOR_VERSION: Final = 1
|
||||||
PATCH_VERSION: Final = "2"
|
PATCH_VERSION: Final = "3"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
||||||
|
@ -13,6 +13,7 @@ import logging
|
|||||||
import math
|
import math
|
||||||
import sys
|
import sys
|
||||||
from timeit import default_timer as timer
|
from timeit import default_timer as timer
|
||||||
|
from types import FunctionType
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
@ -374,6 +375,9 @@ class CachedProperties(type):
|
|||||||
# Check if an _attr_ class attribute exits and move it to __attr_. We check
|
# Check if an _attr_ class attribute exits and move it to __attr_. We check
|
||||||
# __dict__ here because we don't care about _attr_ class attributes in parents.
|
# __dict__ here because we don't care about _attr_ class attributes in parents.
|
||||||
if attr_name in cls.__dict__:
|
if attr_name in cls.__dict__:
|
||||||
|
attr = getattr(cls, attr_name)
|
||||||
|
if isinstance(attr, (FunctionType, property)):
|
||||||
|
raise TypeError(f"Can't override {attr_name} in subclass")
|
||||||
setattr(cls, private_attr_name, getattr(cls, attr_name))
|
setattr(cls, private_attr_name, getattr(cls, attr_name))
|
||||||
annotations = cls.__annotations__
|
annotations = cls.__annotations__
|
||||||
if attr_name in annotations:
|
if attr_name in annotations:
|
||||||
|
@ -14,7 +14,7 @@ bcrypt==4.0.1
|
|||||||
bleak-retry-connector==3.4.0
|
bleak-retry-connector==3.4.0
|
||||||
bleak==0.21.1
|
bleak==0.21.1
|
||||||
bluetooth-adapters==0.16.2
|
bluetooth-adapters==0.16.2
|
||||||
bluetooth-auto-recovery==1.2.3
|
bluetooth-auto-recovery==1.3.0
|
||||||
bluetooth-data-tools==1.19.0
|
bluetooth-data-tools==1.19.0
|
||||||
cached_ipaddress==0.3.0
|
cached_ipaddress==0.3.0
|
||||||
certifi>=2021.5.30
|
certifi>=2021.5.30
|
||||||
@ -24,10 +24,10 @@ dbus-fast==2.21.0
|
|||||||
fnv-hash-fast==0.5.0
|
fnv-hash-fast==0.5.0
|
||||||
ha-av==10.1.1
|
ha-av==10.1.1
|
||||||
ha-ffmpeg==3.1.0
|
ha-ffmpeg==3.1.0
|
||||||
habluetooth==2.0.2
|
habluetooth==2.1.0
|
||||||
hass-nabucasa==0.75.1
|
hass-nabucasa==0.75.1
|
||||||
hassil==1.5.1
|
hassil==1.5.1
|
||||||
home-assistant-bluetooth==1.11.0
|
home-assistant-bluetooth==1.12.0
|
||||||
home-assistant-frontend==20240104.0
|
home-assistant-frontend==20240104.0
|
||||||
home-assistant-intents==2024.1.2
|
home-assistant-intents==2024.1.2
|
||||||
httpx==0.26.0
|
httpx==0.26.0
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2024.1.2"
|
version = "2024.1.3"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
@ -37,7 +37,7 @@ dependencies = [
|
|||||||
# When bumping httpx, please check the version pins of
|
# When bumping httpx, please check the version pins of
|
||||||
# httpcore, anyio, and h11 in gen_requirements_all
|
# httpcore, anyio, and h11 in gen_requirements_all
|
||||||
"httpx==0.26.0",
|
"httpx==0.26.0",
|
||||||
"home-assistant-bluetooth==1.11.0",
|
"home-assistant-bluetooth==1.12.0",
|
||||||
"ifaddr==0.2.0",
|
"ifaddr==0.2.0",
|
||||||
"Jinja2==3.1.2",
|
"Jinja2==3.1.2",
|
||||||
"lru-dict==1.3.0",
|
"lru-dict==1.3.0",
|
||||||
|
@ -15,7 +15,7 @@ bcrypt==4.0.1
|
|||||||
certifi>=2021.5.30
|
certifi>=2021.5.30
|
||||||
ciso8601==2.3.0
|
ciso8601==2.3.0
|
||||||
httpx==0.26.0
|
httpx==0.26.0
|
||||||
home-assistant-bluetooth==1.11.0
|
home-assistant-bluetooth==1.12.0
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
lru-dict==1.3.0
|
lru-dict==1.3.0
|
||||||
|
@ -167,7 +167,7 @@ afsapi==0.2.7
|
|||||||
agent-py==0.0.23
|
agent-py==0.0.23
|
||||||
|
|
||||||
# homeassistant.components.geo_json_events
|
# homeassistant.components.geo_json_events
|
||||||
aio-geojson-generic-client==0.3
|
aio-geojson-generic-client==0.4
|
||||||
|
|
||||||
# homeassistant.components.geonetnz_quakes
|
# homeassistant.components.geonetnz_quakes
|
||||||
aio-geojson-geonetnz-quakes==0.15
|
aio-geojson-geonetnz-quakes==0.15
|
||||||
@ -215,7 +215,7 @@ aiobafi6==0.9.0
|
|||||||
aiobotocore==2.6.0
|
aiobotocore==2.6.0
|
||||||
|
|
||||||
# homeassistant.components.comelit
|
# homeassistant.components.comelit
|
||||||
aiocomelit==0.7.0
|
aiocomelit==0.7.3
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
# homeassistant.components.dhcp
|
||||||
aiodiscover==1.6.0
|
aiodiscover==1.6.0
|
||||||
@ -356,7 +356,7 @@ aioruuvigateway==0.1.0
|
|||||||
aiosenz==1.0.0
|
aiosenz==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.shelly
|
# homeassistant.components.shelly
|
||||||
aioshelly==7.0.0
|
aioshelly==7.1.0
|
||||||
|
|
||||||
# homeassistant.components.skybell
|
# homeassistant.components.skybell
|
||||||
aioskybell==22.7.0
|
aioskybell==22.7.0
|
||||||
@ -368,7 +368,7 @@ aioslimproto==2.3.3
|
|||||||
aiosteamist==0.3.2
|
aiosteamist==0.3.2
|
||||||
|
|
||||||
# homeassistant.components.switcher_kis
|
# homeassistant.components.switcher_kis
|
||||||
aioswitcher==3.3.0
|
aioswitcher==3.4.1
|
||||||
|
|
||||||
# homeassistant.components.syncthing
|
# homeassistant.components.syncthing
|
||||||
aiosyncthing==0.5.1
|
aiosyncthing==0.5.1
|
||||||
@ -547,7 +547,7 @@ bleak==0.21.1
|
|||||||
blebox-uniapi==2.2.0
|
blebox-uniapi==2.2.0
|
||||||
|
|
||||||
# homeassistant.components.blink
|
# homeassistant.components.blink
|
||||||
blinkpy==0.22.4
|
blinkpy==0.22.5
|
||||||
|
|
||||||
# homeassistant.components.bitcoin
|
# homeassistant.components.bitcoin
|
||||||
blockchain==1.4.4
|
blockchain==1.4.4
|
||||||
@ -566,7 +566,7 @@ bluemaestro-ble==0.2.3
|
|||||||
bluetooth-adapters==0.16.2
|
bluetooth-adapters==0.16.2
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-auto-recovery==1.2.3
|
bluetooth-auto-recovery==1.3.0
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
# homeassistant.components.ld2410_ble
|
# homeassistant.components.ld2410_ble
|
||||||
@ -998,7 +998,7 @@ ha-philipsjs==3.1.1
|
|||||||
habitipy==0.2.0
|
habitipy==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
habluetooth==2.0.2
|
habluetooth==2.1.0
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.75.1
|
hass-nabucasa==0.75.1
|
||||||
@ -1240,7 +1240,7 @@ maxcube-api==0.4.3
|
|||||||
mbddns==0.1.2
|
mbddns==0.1.2
|
||||||
|
|
||||||
# homeassistant.components.minecraft_server
|
# homeassistant.components.minecraft_server
|
||||||
mcstatus==11.0.0
|
mcstatus==11.1.1
|
||||||
|
|
||||||
# homeassistant.components.meater
|
# homeassistant.components.meater
|
||||||
meater-python==0.0.8
|
meater-python==0.0.8
|
||||||
@ -1548,7 +1548,7 @@ pushover_complete==1.1.1
|
|||||||
pvo==2.1.1
|
pvo==2.1.1
|
||||||
|
|
||||||
# homeassistant.components.aosmith
|
# homeassistant.components.aosmith
|
||||||
py-aosmith==1.0.1
|
py-aosmith==1.0.4
|
||||||
|
|
||||||
# homeassistant.components.canary
|
# homeassistant.components.canary
|
||||||
py-canary==0.5.3
|
py-canary==0.5.3
|
||||||
@ -2280,7 +2280,7 @@ pytrydan==0.4.0
|
|||||||
pyudev==0.23.2
|
pyudev==0.23.2
|
||||||
|
|
||||||
# homeassistant.components.unifiprotect
|
# homeassistant.components.unifiprotect
|
||||||
pyunifiprotect==4.22.5
|
pyunifiprotect==4.23.2
|
||||||
|
|
||||||
# homeassistant.components.uptimerobot
|
# homeassistant.components.uptimerobot
|
||||||
pyuptimerobot==22.2.0
|
pyuptimerobot==22.2.0
|
||||||
@ -2376,7 +2376,7 @@ renault-api==0.2.1
|
|||||||
renson-endura-delta==1.7.1
|
renson-endura-delta==1.7.1
|
||||||
|
|
||||||
# homeassistant.components.reolink
|
# homeassistant.components.reolink
|
||||||
reolink-aio==0.8.5
|
reolink-aio==0.8.6
|
||||||
|
|
||||||
# homeassistant.components.idteck_prox
|
# homeassistant.components.idteck_prox
|
||||||
rfk101py==0.0.1
|
rfk101py==0.0.1
|
||||||
@ -2890,7 +2890,7 @@ zigpy-znp==0.12.1
|
|||||||
zigpy==0.60.4
|
zigpy==0.60.4
|
||||||
|
|
||||||
# homeassistant.components.zoneminder
|
# homeassistant.components.zoneminder
|
||||||
zm-py==0.5.2
|
zm-py==0.5.4
|
||||||
|
|
||||||
# homeassistant.components.zwave_js
|
# homeassistant.components.zwave_js
|
||||||
zwave-js-server-python==0.55.3
|
zwave-js-server-python==0.55.3
|
||||||
|
@ -146,7 +146,7 @@ afsapi==0.2.7
|
|||||||
agent-py==0.0.23
|
agent-py==0.0.23
|
||||||
|
|
||||||
# homeassistant.components.geo_json_events
|
# homeassistant.components.geo_json_events
|
||||||
aio-geojson-generic-client==0.3
|
aio-geojson-generic-client==0.4
|
||||||
|
|
||||||
# homeassistant.components.geonetnz_quakes
|
# homeassistant.components.geonetnz_quakes
|
||||||
aio-geojson-geonetnz-quakes==0.15
|
aio-geojson-geonetnz-quakes==0.15
|
||||||
@ -194,7 +194,7 @@ aiobafi6==0.9.0
|
|||||||
aiobotocore==2.6.0
|
aiobotocore==2.6.0
|
||||||
|
|
||||||
# homeassistant.components.comelit
|
# homeassistant.components.comelit
|
||||||
aiocomelit==0.7.0
|
aiocomelit==0.7.3
|
||||||
|
|
||||||
# homeassistant.components.dhcp
|
# homeassistant.components.dhcp
|
||||||
aiodiscover==1.6.0
|
aiodiscover==1.6.0
|
||||||
@ -329,7 +329,7 @@ aioruuvigateway==0.1.0
|
|||||||
aiosenz==1.0.0
|
aiosenz==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.shelly
|
# homeassistant.components.shelly
|
||||||
aioshelly==7.0.0
|
aioshelly==7.1.0
|
||||||
|
|
||||||
# homeassistant.components.skybell
|
# homeassistant.components.skybell
|
||||||
aioskybell==22.7.0
|
aioskybell==22.7.0
|
||||||
@ -341,7 +341,7 @@ aioslimproto==2.3.3
|
|||||||
aiosteamist==0.3.2
|
aiosteamist==0.3.2
|
||||||
|
|
||||||
# homeassistant.components.switcher_kis
|
# homeassistant.components.switcher_kis
|
||||||
aioswitcher==3.3.0
|
aioswitcher==3.4.1
|
||||||
|
|
||||||
# homeassistant.components.syncthing
|
# homeassistant.components.syncthing
|
||||||
aiosyncthing==0.5.1
|
aiosyncthing==0.5.1
|
||||||
@ -466,7 +466,7 @@ bleak==0.21.1
|
|||||||
blebox-uniapi==2.2.0
|
blebox-uniapi==2.2.0
|
||||||
|
|
||||||
# homeassistant.components.blink
|
# homeassistant.components.blink
|
||||||
blinkpy==0.22.4
|
blinkpy==0.22.5
|
||||||
|
|
||||||
# homeassistant.components.blue_current
|
# homeassistant.components.blue_current
|
||||||
bluecurrent-api==1.0.6
|
bluecurrent-api==1.0.6
|
||||||
@ -478,7 +478,7 @@ bluemaestro-ble==0.2.3
|
|||||||
bluetooth-adapters==0.16.2
|
bluetooth-adapters==0.16.2
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
bluetooth-auto-recovery==1.2.3
|
bluetooth-auto-recovery==1.3.0
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
# homeassistant.components.ld2410_ble
|
# homeassistant.components.ld2410_ble
|
||||||
@ -803,7 +803,7 @@ ha-philipsjs==3.1.1
|
|||||||
habitipy==0.2.0
|
habitipy==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.bluetooth
|
# homeassistant.components.bluetooth
|
||||||
habluetooth==2.0.2
|
habluetooth==2.1.0
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.75.1
|
hass-nabucasa==0.75.1
|
||||||
@ -976,7 +976,7 @@ maxcube-api==0.4.3
|
|||||||
mbddns==0.1.2
|
mbddns==0.1.2
|
||||||
|
|
||||||
# homeassistant.components.minecraft_server
|
# homeassistant.components.minecraft_server
|
||||||
mcstatus==11.0.0
|
mcstatus==11.1.1
|
||||||
|
|
||||||
# homeassistant.components.meater
|
# homeassistant.components.meater
|
||||||
meater-python==0.0.8
|
meater-python==0.0.8
|
||||||
@ -1195,7 +1195,7 @@ pushover_complete==1.1.1
|
|||||||
pvo==2.1.1
|
pvo==2.1.1
|
||||||
|
|
||||||
# homeassistant.components.aosmith
|
# homeassistant.components.aosmith
|
||||||
py-aosmith==1.0.1
|
py-aosmith==1.0.4
|
||||||
|
|
||||||
# homeassistant.components.canary
|
# homeassistant.components.canary
|
||||||
py-canary==0.5.3
|
py-canary==0.5.3
|
||||||
@ -1726,7 +1726,7 @@ pytrydan==0.4.0
|
|||||||
pyudev==0.23.2
|
pyudev==0.23.2
|
||||||
|
|
||||||
# homeassistant.components.unifiprotect
|
# homeassistant.components.unifiprotect
|
||||||
pyunifiprotect==4.22.5
|
pyunifiprotect==4.23.2
|
||||||
|
|
||||||
# homeassistant.components.uptimerobot
|
# homeassistant.components.uptimerobot
|
||||||
pyuptimerobot==22.2.0
|
pyuptimerobot==22.2.0
|
||||||
@ -1798,7 +1798,7 @@ renault-api==0.2.1
|
|||||||
renson-endura-delta==1.7.1
|
renson-endura-delta==1.7.1
|
||||||
|
|
||||||
# homeassistant.components.reolink
|
# homeassistant.components.reolink
|
||||||
reolink-aio==0.8.5
|
reolink-aio==0.8.6
|
||||||
|
|
||||||
# homeassistant.components.rflink
|
# homeassistant.components.rflink
|
||||||
rflink==0.0.65
|
rflink==0.0.65
|
||||||
|
@ -224,9 +224,20 @@ class ReportedProperties:
|
|||||||
|
|
||||||
def assert_equal(self, namespace, name, value):
|
def assert_equal(self, namespace, name, value):
|
||||||
"""Assert a property is equal to a given value."""
|
"""Assert a property is equal to a given value."""
|
||||||
|
prop_set = None
|
||||||
|
prop_count = 0
|
||||||
for prop in self.properties:
|
for prop in self.properties:
|
||||||
if prop["namespace"] == namespace and prop["name"] == name:
|
if prop["namespace"] == namespace and prop["name"] == name:
|
||||||
assert prop["value"] == value
|
assert prop["value"] == value
|
||||||
return prop
|
prop_set = prop
|
||||||
|
prop_count += 1
|
||||||
|
|
||||||
|
if prop_count > 1:
|
||||||
|
pytest.fail(
|
||||||
|
f"property {namespace}:{name} more than once in {self.properties!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if prop_set:
|
||||||
|
return prop_set
|
||||||
|
|
||||||
pytest.fail(f"property {namespace}:{name} not in {self.properties!r}")
|
pytest.fail(f"property {namespace}:{name} not in {self.properties!r}")
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Test Smart Home HTTP endpoints."""
|
"""Test Smart Home HTTP endpoints."""
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -44,11 +45,16 @@ async def do_http_discovery(config, hass, hass_client):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_http_api(
|
async def test_http_api(
|
||||||
hass: HomeAssistant, hass_client: ClientSessionGenerator, config: dict[str, Any]
|
hass: HomeAssistant,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
hass_client: ClientSessionGenerator,
|
||||||
|
config: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""With `smart_home:` HTTP API is exposed."""
|
"""With `smart_home:` HTTP API is exposed and debug log is redacted."""
|
||||||
response = await do_http_discovery(config, hass, hass_client)
|
with caplog.at_level(logging.DEBUG):
|
||||||
response_data = await response.json()
|
response = await do_http_discovery(config, hass, hass_client)
|
||||||
|
response_data = await response.json()
|
||||||
|
assert "'correlationToken': '**REDACTED**'" in caplog.text
|
||||||
|
|
||||||
# Here we're testing just the HTTP view glue -- details of discovery are
|
# Here we're testing just the HTTP view glue -- details of discovery are
|
||||||
# covered in other tests.
|
# covered in other tests.
|
||||||
@ -61,5 +67,4 @@ async def test_http_api_disabled(
|
|||||||
"""Without `smart_home:`, the HTTP API is disabled."""
|
"""Without `smart_home:`, the HTTP API is disabled."""
|
||||||
config = {"alexa": {}}
|
config = {"alexa": {}}
|
||||||
response = await do_http_discovery(config, hass, hass_client)
|
response = await do_http_discovery(config, hass, hass_client)
|
||||||
|
|
||||||
assert response.status == HTTPStatus.NOT_FOUND
|
assert response.status == HTTPStatus.NOT_FOUND
|
||||||
|
@ -54,10 +54,16 @@ async def mock_client(get_devices_fixture: str) -> Generator[MagicMock, None, No
|
|||||||
get_energy_use_fixture = load_json_object_fixture(
|
get_energy_use_fixture = load_json_object_fixture(
|
||||||
"get_energy_use_data.json", DOMAIN
|
"get_energy_use_data.json", DOMAIN
|
||||||
)
|
)
|
||||||
|
get_all_device_info_fixture = load_json_object_fixture(
|
||||||
|
"get_all_device_info.json", DOMAIN
|
||||||
|
)
|
||||||
|
|
||||||
client_mock = MagicMock(AOSmithAPIClient)
|
client_mock = MagicMock(AOSmithAPIClient)
|
||||||
client_mock.get_devices = AsyncMock(return_value=get_devices_fixture)
|
client_mock.get_devices = AsyncMock(return_value=get_devices_fixture)
|
||||||
client_mock.get_energy_use_data = AsyncMock(return_value=get_energy_use_fixture)
|
client_mock.get_energy_use_data = AsyncMock(return_value=get_energy_use_fixture)
|
||||||
|
client_mock.get_all_device_info = AsyncMock(
|
||||||
|
return_value=get_all_device_info_fixture
|
||||||
|
)
|
||||||
|
|
||||||
return client_mock
|
return client_mock
|
||||||
|
|
||||||
|
247
tests/components/aosmith/fixtures/get_all_device_info.json
Normal file
247
tests/components/aosmith/fixtures/get_all_device_info.json
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
{
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"alertSettings": {
|
||||||
|
"faultCode": {
|
||||||
|
"major": {
|
||||||
|
"email": true,
|
||||||
|
"sms": false
|
||||||
|
},
|
||||||
|
"minor": {
|
||||||
|
"email": false,
|
||||||
|
"sms": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"operatingSetPoint": {
|
||||||
|
"email": false,
|
||||||
|
"sms": false
|
||||||
|
},
|
||||||
|
"tankTemperature": {
|
||||||
|
"highTemperature": {
|
||||||
|
"email": false,
|
||||||
|
"sms": false,
|
||||||
|
"value": 160
|
||||||
|
},
|
||||||
|
"lowTemperature": {
|
||||||
|
"email": false,
|
||||||
|
"sms": false,
|
||||||
|
"value": 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"brand": "aosmith",
|
||||||
|
"deviceType": "NEXT_GEN_HEAT_PUMP",
|
||||||
|
"dsn": "dsn",
|
||||||
|
"hardware": {
|
||||||
|
"hasBluetooth": true,
|
||||||
|
"interface": "CONTROL_PANEL"
|
||||||
|
},
|
||||||
|
"id": "id",
|
||||||
|
"install": {
|
||||||
|
"address": "sample_address",
|
||||||
|
"city": "sample_city",
|
||||||
|
"country": "United States",
|
||||||
|
"date": "2023-09-29",
|
||||||
|
"email": "sample_email",
|
||||||
|
"group": "Residential",
|
||||||
|
"location": "Basement",
|
||||||
|
"phone": "sample_phone",
|
||||||
|
"postalCode": "sample_postal_code",
|
||||||
|
"professional": false,
|
||||||
|
"registeredOwner": "sample_owner",
|
||||||
|
"registrationDate": "2023-12-24",
|
||||||
|
"state": "sample_state"
|
||||||
|
},
|
||||||
|
"isRegistered": true,
|
||||||
|
"junctionId": "junctionId",
|
||||||
|
"lastUpdate": 1703386473737,
|
||||||
|
"model": "HPTS-50 200 202172000",
|
||||||
|
"name": "Water Heater",
|
||||||
|
"permissions": "USER",
|
||||||
|
"productId": "100350404",
|
||||||
|
"serial": "sample_serial",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"contactId": "sample_contact_id",
|
||||||
|
"email": "sample_email",
|
||||||
|
"firstName": "sample_first_name",
|
||||||
|
"isSelf": true,
|
||||||
|
"lastName": "sample_last_name",
|
||||||
|
"permissions": "USER"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data": {
|
||||||
|
"activeAlerts": [],
|
||||||
|
"alertHistory": [],
|
||||||
|
"isOnline": true,
|
||||||
|
"isWifi": true,
|
||||||
|
"lastUpdate": 1703138389000,
|
||||||
|
"signalStrength": null,
|
||||||
|
"heaterSsid": "sample_heater_ssid",
|
||||||
|
"ssid": "sample_ssid",
|
||||||
|
"temperatureSetpoint": 145,
|
||||||
|
"temperatureSetpointPending": false,
|
||||||
|
"temperatureSetpointPrevious": 145,
|
||||||
|
"temperatureSetpointMaximum": 145,
|
||||||
|
"error": "",
|
||||||
|
"modes": [
|
||||||
|
{
|
||||||
|
"mode": "HYBRID",
|
||||||
|
"controls": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "HEAT_PUMP",
|
||||||
|
"controls": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "ELECTRIC",
|
||||||
|
"controls": "SELECT_DAYS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mode": "VACATION",
|
||||||
|
"controls": "SELECT_DAYS"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"firmwareVersion": "2.14",
|
||||||
|
"hotWaterStatus": "HIGH",
|
||||||
|
"isAdvancedLoadUpMore": false,
|
||||||
|
"isCtaUcmPresent": false,
|
||||||
|
"isDemandResponsePaused": false,
|
||||||
|
"isEnrolled": false,
|
||||||
|
"mode": "HEAT_PUMP",
|
||||||
|
"modePending": false,
|
||||||
|
"vacationModeRemainingDays": 0,
|
||||||
|
"electricModeRemainingDays": 100,
|
||||||
|
"isLowes": false,
|
||||||
|
"canEditTimeOfUse": false,
|
||||||
|
"timeOfUseData": null,
|
||||||
|
"consumerScheduleData": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"energy_use_data": {
|
||||||
|
"junctionId": {
|
||||||
|
"average": 2.4744000000000006,
|
||||||
|
"graphData": [
|
||||||
|
{
|
||||||
|
"date": "2023-11-26T04:00:00.000Z",
|
||||||
|
"kwh": 0.936
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-11-27T04:00:00.000Z",
|
||||||
|
"kwh": 4.248
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-11-28T04:00:00.000Z",
|
||||||
|
"kwh": 1.002
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-11-29T04:00:00.000Z",
|
||||||
|
"kwh": 3.078
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-11-30T04:00:00.000Z",
|
||||||
|
"kwh": 1.896
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-01T04:00:00.000Z",
|
||||||
|
"kwh": 1.98
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-02T04:00:00.000Z",
|
||||||
|
"kwh": 2.112
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-03T04:00:00.000Z",
|
||||||
|
"kwh": 3.222
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-04T04:00:00.000Z",
|
||||||
|
"kwh": 4.254
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-05T04:00:00.000Z",
|
||||||
|
"kwh": 4.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-06T04:00:00.000Z",
|
||||||
|
"kwh": 3.312
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-07T04:00:00.000Z",
|
||||||
|
"kwh": 2.334
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-08T04:00:00.000Z",
|
||||||
|
"kwh": 2.418
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-09T04:00:00.000Z",
|
||||||
|
"kwh": 2.19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-10T04:00:00.000Z",
|
||||||
|
"kwh": 3.786
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-11T04:00:00.000Z",
|
||||||
|
"kwh": 5.292
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-12T04:00:00.000Z",
|
||||||
|
"kwh": 1.38
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-13T04:00:00.000Z",
|
||||||
|
"kwh": 3.324
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-14T04:00:00.000Z",
|
||||||
|
"kwh": 1.092
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-15T04:00:00.000Z",
|
||||||
|
"kwh": 0.606
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-16T04:00:00.000Z",
|
||||||
|
"kwh": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-17T04:00:00.000Z",
|
||||||
|
"kwh": 2.838
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-18T04:00:00.000Z",
|
||||||
|
"kwh": 2.382
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-19T04:00:00.000Z",
|
||||||
|
"kwh": 2.904
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-20T04:00:00.000Z",
|
||||||
|
"kwh": 1.914
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-21T04:00:00.000Z",
|
||||||
|
"kwh": 3.93
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-22T04:00:00.000Z",
|
||||||
|
"kwh": 3.666
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-23T04:00:00.000Z",
|
||||||
|
"kwh": 2.766
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2023-12-24T04:00:00.000Z",
|
||||||
|
"kwh": 1.32
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lifetimeKwh": 203.259,
|
||||||
|
"startDate": "Nov 26"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
252
tests/components/aosmith/snapshots/test_diagnostics.ambr
Normal file
252
tests/components/aosmith/snapshots/test_diagnostics.ambr
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_diagnostics
|
||||||
|
dict({
|
||||||
|
'devices': list([
|
||||||
|
dict({
|
||||||
|
'alertSettings': dict({
|
||||||
|
'faultCode': dict({
|
||||||
|
'major': dict({
|
||||||
|
'email': '**REDACTED**',
|
||||||
|
'sms': False,
|
||||||
|
}),
|
||||||
|
'minor': dict({
|
||||||
|
'email': '**REDACTED**',
|
||||||
|
'sms': False,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'operatingSetPoint': dict({
|
||||||
|
'email': '**REDACTED**',
|
||||||
|
'sms': False,
|
||||||
|
}),
|
||||||
|
'tankTemperature': dict({
|
||||||
|
'highTemperature': dict({
|
||||||
|
'email': '**REDACTED**',
|
||||||
|
'sms': False,
|
||||||
|
'value': 160,
|
||||||
|
}),
|
||||||
|
'lowTemperature': dict({
|
||||||
|
'email': '**REDACTED**',
|
||||||
|
'sms': False,
|
||||||
|
'value': 120,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'brand': 'aosmith',
|
||||||
|
'data': dict({
|
||||||
|
'activeAlerts': list([
|
||||||
|
]),
|
||||||
|
'alertHistory': list([
|
||||||
|
]),
|
||||||
|
'canEditTimeOfUse': False,
|
||||||
|
'consumerScheduleData': None,
|
||||||
|
'electricModeRemainingDays': 100,
|
||||||
|
'error': '',
|
||||||
|
'firmwareVersion': '2.14',
|
||||||
|
'heaterSsid': '**REDACTED**',
|
||||||
|
'hotWaterStatus': 'HIGH',
|
||||||
|
'isAdvancedLoadUpMore': False,
|
||||||
|
'isCtaUcmPresent': False,
|
||||||
|
'isDemandResponsePaused': False,
|
||||||
|
'isEnrolled': False,
|
||||||
|
'isLowes': False,
|
||||||
|
'isOnline': True,
|
||||||
|
'isWifi': True,
|
||||||
|
'lastUpdate': 1703138389000,
|
||||||
|
'mode': 'HEAT_PUMP',
|
||||||
|
'modePending': False,
|
||||||
|
'modes': list([
|
||||||
|
dict({
|
||||||
|
'controls': None,
|
||||||
|
'mode': 'HYBRID',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'controls': None,
|
||||||
|
'mode': 'HEAT_PUMP',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'controls': 'SELECT_DAYS',
|
||||||
|
'mode': 'ELECTRIC',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'controls': 'SELECT_DAYS',
|
||||||
|
'mode': 'VACATION',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
'signalStrength': None,
|
||||||
|
'ssid': '**REDACTED**',
|
||||||
|
'temperatureSetpoint': 145,
|
||||||
|
'temperatureSetpointMaximum': 145,
|
||||||
|
'temperatureSetpointPending': False,
|
||||||
|
'temperatureSetpointPrevious': 145,
|
||||||
|
'timeOfUseData': None,
|
||||||
|
'vacationModeRemainingDays': 0,
|
||||||
|
}),
|
||||||
|
'deviceType': 'NEXT_GEN_HEAT_PUMP',
|
||||||
|
'dsn': '**REDACTED**',
|
||||||
|
'hardware': dict({
|
||||||
|
'hasBluetooth': True,
|
||||||
|
'interface': 'CONTROL_PANEL',
|
||||||
|
}),
|
||||||
|
'id': '**REDACTED**',
|
||||||
|
'install': dict({
|
||||||
|
'address': '**REDACTED**',
|
||||||
|
'city': '**REDACTED**',
|
||||||
|
'country': 'United States',
|
||||||
|
'date': '2023-09-29',
|
||||||
|
'email': '**REDACTED**',
|
||||||
|
'group': 'Residential',
|
||||||
|
'location': 'Basement',
|
||||||
|
'phone': '**REDACTED**',
|
||||||
|
'postalCode': '**REDACTED**',
|
||||||
|
'professional': False,
|
||||||
|
'registeredOwner': '**REDACTED**',
|
||||||
|
'registrationDate': '2023-12-24',
|
||||||
|
'state': '**REDACTED**',
|
||||||
|
}),
|
||||||
|
'isRegistered': True,
|
||||||
|
'junctionId': 'junctionId',
|
||||||
|
'lastUpdate': 1703386473737,
|
||||||
|
'model': 'HPTS-50 200 202172000',
|
||||||
|
'name': 'Water Heater',
|
||||||
|
'permissions': 'USER',
|
||||||
|
'productId': '100350404',
|
||||||
|
'serial': '**REDACTED**',
|
||||||
|
'users': list([
|
||||||
|
dict({
|
||||||
|
'contactId': '**REDACTED**',
|
||||||
|
'email': '**REDACTED**',
|
||||||
|
'firstName': '**REDACTED**',
|
||||||
|
'isSelf': True,
|
||||||
|
'lastName': '**REDACTED**',
|
||||||
|
'permissions': 'USER',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
'energy_use_data': dict({
|
||||||
|
'junctionId': dict({
|
||||||
|
'average': 2.4744000000000006,
|
||||||
|
'graphData': list([
|
||||||
|
dict({
|
||||||
|
'date': '2023-11-26T04:00:00.000Z',
|
||||||
|
'kwh': 0.936,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-11-27T04:00:00.000Z',
|
||||||
|
'kwh': 4.248,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-11-28T04:00:00.000Z',
|
||||||
|
'kwh': 1.002,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-11-29T04:00:00.000Z',
|
||||||
|
'kwh': 3.078,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-11-30T04:00:00.000Z',
|
||||||
|
'kwh': 1.896,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-01T04:00:00.000Z',
|
||||||
|
'kwh': 1.98,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-02T04:00:00.000Z',
|
||||||
|
'kwh': 2.112,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-03T04:00:00.000Z',
|
||||||
|
'kwh': 3.222,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-04T04:00:00.000Z',
|
||||||
|
'kwh': 4.254,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-05T04:00:00.000Z',
|
||||||
|
'kwh': 4.05,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-06T04:00:00.000Z',
|
||||||
|
'kwh': 3.312,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-07T04:00:00.000Z',
|
||||||
|
'kwh': 2.334,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-08T04:00:00.000Z',
|
||||||
|
'kwh': 2.418,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-09T04:00:00.000Z',
|
||||||
|
'kwh': 2.19,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-10T04:00:00.000Z',
|
||||||
|
'kwh': 3.786,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-11T04:00:00.000Z',
|
||||||
|
'kwh': 5.292,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-12T04:00:00.000Z',
|
||||||
|
'kwh': 1.38,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-13T04:00:00.000Z',
|
||||||
|
'kwh': 3.324,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-14T04:00:00.000Z',
|
||||||
|
'kwh': 1.092,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-15T04:00:00.000Z',
|
||||||
|
'kwh': 0.606,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-16T04:00:00.000Z',
|
||||||
|
'kwh': 0,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-17T04:00:00.000Z',
|
||||||
|
'kwh': 2.838,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-18T04:00:00.000Z',
|
||||||
|
'kwh': 2.382,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-19T04:00:00.000Z',
|
||||||
|
'kwh': 2.904,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-20T04:00:00.000Z',
|
||||||
|
'kwh': 1.914,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-21T04:00:00.000Z',
|
||||||
|
'kwh': 3.93,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-22T04:00:00.000Z',
|
||||||
|
'kwh': 3.666,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-23T04:00:00.000Z',
|
||||||
|
'kwh': 2.766,
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'date': '2023-12-24T04:00:00.000Z',
|
||||||
|
'kwh': 1.32,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
'lifetimeKwh': 203.259,
|
||||||
|
'startDate': 'Nov 26',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
# ---
|
23
tests/components/aosmith/test_diagnostics.py
Normal file
23
tests/components/aosmith/test_diagnostics.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"""Tests for the diagnostics data provided by the A. O. Smith integration."""
|
||||||
|
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||||
|
from tests.typing import ClientSessionGenerator
|
||||||
|
|
||||||
|
|
||||||
|
async def test_diagnostics(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: ClientSessionGenerator,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test diagnostics."""
|
||||||
|
|
||||||
|
assert (
|
||||||
|
await get_diagnostics_for_config_entry(hass, hass_client, init_integration)
|
||||||
|
== snapshot
|
||||||
|
)
|
@ -55,7 +55,7 @@ def macos_adapter():
|
|||||||
), patch(
|
), patch(
|
||||||
"bluetooth_adapters.systems.platform.system",
|
"bluetooth_adapters.systems.platform.system",
|
||||||
return_value="Darwin",
|
return_value="Darwin",
|
||||||
):
|
), patch("habluetooth.scanner.SYSTEM", "Darwin"):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ def windows_adapter():
|
|||||||
with patch(
|
with patch(
|
||||||
"bluetooth_adapters.systems.platform.system",
|
"bluetooth_adapters.systems.platform.system",
|
||||||
return_value="Windows",
|
return_value="Windows",
|
||||||
):
|
), patch("habluetooth.scanner.SYSTEM", "Windows"):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ def no_adapter_fixture():
|
|||||||
), patch(
|
), patch(
|
||||||
"bluetooth_adapters.systems.platform.system",
|
"bluetooth_adapters.systems.platform.system",
|
||||||
return_value="Linux",
|
return_value="Linux",
|
||||||
), patch(
|
), patch("habluetooth.scanner.SYSTEM", "Linux"), patch(
|
||||||
"bluetooth_adapters.systems.linux.LinuxAdapters.refresh",
|
"bluetooth_adapters.systems.linux.LinuxAdapters.refresh",
|
||||||
), patch(
|
), patch(
|
||||||
"bluetooth_adapters.systems.linux.LinuxAdapters.adapters",
|
"bluetooth_adapters.systems.linux.LinuxAdapters.adapters",
|
||||||
@ -102,7 +102,7 @@ def one_adapter_fixture():
|
|||||||
), patch(
|
), patch(
|
||||||
"bluetooth_adapters.systems.platform.system",
|
"bluetooth_adapters.systems.platform.system",
|
||||||
return_value="Linux",
|
return_value="Linux",
|
||||||
), patch(
|
), patch("habluetooth.scanner.SYSTEM", "Linux"), patch(
|
||||||
"bluetooth_adapters.systems.linux.LinuxAdapters.refresh",
|
"bluetooth_adapters.systems.linux.LinuxAdapters.refresh",
|
||||||
), patch(
|
), patch(
|
||||||
"bluetooth_adapters.systems.linux.LinuxAdapters.adapters",
|
"bluetooth_adapters.systems.linux.LinuxAdapters.adapters",
|
||||||
|
@ -571,6 +571,7 @@ async def test_restart_takes_longer_than_watchdog_time(
|
|||||||
assert "already restarting" in caplog.text
|
assert "already restarting" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif("platform.system() != 'Darwin'")
|
||||||
async def test_setup_and_stop_macos(
|
async def test_setup_and_stop_macos(
|
||||||
hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None
|
hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -76,16 +76,9 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock, None]:
|
|||||||
|
|
||||||
# Attributes that we mock with default values.
|
# Attributes that we mock with default values.
|
||||||
|
|
||||||
mock_cloud.id_token = jwt.encode(
|
mock_cloud.id_token = None
|
||||||
{
|
mock_cloud.access_token = None
|
||||||
"email": "hello@home-assistant.io",
|
mock_cloud.refresh_token = None
|
||||||
"custom:sub-exp": "2018-01-03",
|
|
||||||
"cognito:username": "abcdefghjkl",
|
|
||||||
},
|
|
||||||
"test",
|
|
||||||
)
|
|
||||||
mock_cloud.access_token = "test_access_token"
|
|
||||||
mock_cloud.refresh_token = "test_refresh_token"
|
|
||||||
|
|
||||||
# Properties that we keep as properties.
|
# Properties that we keep as properties.
|
||||||
|
|
||||||
@ -122,11 +115,31 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock, None]:
|
|||||||
|
|
||||||
When called, it should call the on_start callback.
|
When called, it should call the on_start callback.
|
||||||
"""
|
"""
|
||||||
|
mock_cloud.id_token = jwt.encode(
|
||||||
|
{
|
||||||
|
"email": "hello@home-assistant.io",
|
||||||
|
"custom:sub-exp": "2018-01-03",
|
||||||
|
"cognito:username": "abcdefghjkl",
|
||||||
|
},
|
||||||
|
"test",
|
||||||
|
)
|
||||||
|
mock_cloud.access_token = "test_access_token"
|
||||||
|
mock_cloud.refresh_token = "test_refresh_token"
|
||||||
on_start_callback = mock_cloud.register_on_start.call_args[0][0]
|
on_start_callback = mock_cloud.register_on_start.call_args[0][0]
|
||||||
await on_start_callback()
|
await on_start_callback()
|
||||||
|
|
||||||
mock_cloud.login.side_effect = mock_login
|
mock_cloud.login.side_effect = mock_login
|
||||||
|
|
||||||
|
async def mock_logout() -> None:
|
||||||
|
"""Mock logout."""
|
||||||
|
mock_cloud.id_token = None
|
||||||
|
mock_cloud.access_token = None
|
||||||
|
mock_cloud.refresh_token = None
|
||||||
|
await mock_cloud.stop()
|
||||||
|
await mock_cloud.client.logout_cleanups()
|
||||||
|
|
||||||
|
mock_cloud.logout.side_effect = mock_logout
|
||||||
|
|
||||||
yield mock_cloud
|
yield mock_cloud
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,8 +113,8 @@ async def setup_cloud_fixture(hass: HomeAssistant, cloud: MagicMock) -> None:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
on_start_callback = cloud.register_on_start.call_args[0][0]
|
await cloud.login("test-user", "test-pass")
|
||||||
await on_start_callback()
|
cloud.login.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
async def test_google_actions_sync(
|
async def test_google_actions_sync(
|
||||||
|
@ -19,7 +19,7 @@ from homeassistant.core import Context, HomeAssistant
|
|||||||
from homeassistant.exceptions import Unauthorized
|
from homeassistant.exceptions import Unauthorized
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import MockUser
|
from tests.common import MockConfigEntry, MockUser
|
||||||
|
|
||||||
|
|
||||||
async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None:
|
async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None:
|
||||||
@ -230,6 +230,7 @@ async def test_async_get_or_create_cloudhook(
|
|||||||
"""Test async_get_or_create_cloudhook."""
|
"""Test async_get_or_create_cloudhook."""
|
||||||
assert await async_setup_component(hass, "cloud", {"cloud": {}})
|
assert await async_setup_component(hass, "cloud", {"cloud": {}})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
await cloud.login("test-user", "test-pass")
|
||||||
|
|
||||||
webhook_id = "mock-webhook-id"
|
webhook_id = "mock-webhook-id"
|
||||||
cloudhook_url = "https://cloudhook.nabu.casa/abcdefg"
|
cloudhook_url = "https://cloudhook.nabu.casa/abcdefg"
|
||||||
@ -262,7 +263,7 @@ async def test_async_get_or_create_cloudhook(
|
|||||||
async_create_cloudhook_mock.assert_not_called()
|
async_create_cloudhook_mock.assert_not_called()
|
||||||
|
|
||||||
# Simulate logged out
|
# Simulate logged out
|
||||||
cloud.id_token = None
|
await cloud.logout()
|
||||||
|
|
||||||
# Not logged in
|
# Not logged in
|
||||||
with pytest.raises(CloudNotAvailable):
|
with pytest.raises(CloudNotAvailable):
|
||||||
@ -274,3 +275,18 @@ async def test_async_get_or_create_cloudhook(
|
|||||||
# Not connected
|
# Not connected
|
||||||
with pytest.raises(CloudNotConnected):
|
with pytest.raises(CloudNotConnected):
|
||||||
await async_get_or_create_cloudhook(hass, webhook_id)
|
await async_get_or_create_cloudhook(hass, webhook_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cloud_logout(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cloud: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test cloud setup with existing config entry when user is logged out."""
|
||||||
|
assert cloud.is_logged_in is False
|
||||||
|
|
||||||
|
mock_config_entry = MockConfigEntry(domain=DOMAIN)
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {"cloud": {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert cloud.is_logged_in is False
|
||||||
|
@ -42,6 +42,7 @@ async def test_cloud_system_health(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
await cloud.login("test-user", "test-pass")
|
||||||
|
|
||||||
cloud.remote.snitun_server = "us-west-1"
|
cloud.remote.snitun_server = "us-west-1"
|
||||||
cloud.remote.certificate_status = CertificateStatus.READY
|
cloud.remote.certificate_status = CertificateStatus.READY
|
||||||
|
@ -4,7 +4,7 @@ from http import HTTPStatus
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
from hass_nabucasa.voice import MAP_VOICE, VoiceError
|
from hass_nabucasa.voice import MAP_VOICE, VoiceError, VoiceTokenError
|
||||||
import pytest
|
import pytest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -189,3 +189,55 @@ async def test_get_tts_audio(
|
|||||||
assert mock_process_tts.call_args.kwargs["language"] == "en-US"
|
assert mock_process_tts.call_args.kwargs["language"] == "en-US"
|
||||||
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
||||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("data", "expected_url_suffix"),
|
||||||
|
[
|
||||||
|
({"platform": DOMAIN}, DOMAIN),
|
||||||
|
({"engine_id": DOMAIN}, DOMAIN),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_get_tts_audio_logged_out(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client: ClientSessionGenerator,
|
||||||
|
cloud: MagicMock,
|
||||||
|
data: dict[str, Any],
|
||||||
|
expected_url_suffix: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test cloud get tts audio when user is logged out."""
|
||||||
|
mock_process_tts = AsyncMock(
|
||||||
|
side_effect=VoiceTokenError("No token!"),
|
||||||
|
)
|
||||||
|
cloud.voice.process_tts = mock_process_tts
|
||||||
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
client = await hass_client()
|
||||||
|
|
||||||
|
url = "/api/tts_get_url"
|
||||||
|
data |= {"message": "There is someone at the door."}
|
||||||
|
|
||||||
|
req = await client.post(url, json=data)
|
||||||
|
assert req.status == HTTPStatus.OK
|
||||||
|
response = await req.json()
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"url": (
|
||||||
|
"http://example.local:8123/api/tts_proxy/"
|
||||||
|
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
|
f"_en-us_e09b5a0968_{expected_url_suffix}.mp3"
|
||||||
|
),
|
||||||
|
"path": (
|
||||||
|
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||||
|
f"_en-us_e09b5a0968_{expected_url_suffix}.mp3"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_process_tts.call_count == 1
|
||||||
|
assert mock_process_tts.call_args is not None
|
||||||
|
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||||
|
assert mock_process_tts.call_args.kwargs["language"] == "en-US"
|
||||||
|
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
||||||
|
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||||
|
@ -34,7 +34,8 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) -
|
|||||||
# ent3 = cover with simple tilt functions and no position
|
# ent3 = cover with simple tilt functions and no position
|
||||||
# ent4 = cover with all tilt functions but no position
|
# ent4 = cover with all tilt functions but no position
|
||||||
# ent5 = cover with all functions
|
# ent5 = cover with all functions
|
||||||
ent1, ent2, ent3, ent4, ent5 = platform.ENTITIES
|
# ent6 = cover with only open/close, but also reports opening/closing
|
||||||
|
ent1, ent2, ent3, ent4, ent5, ent6 = platform.ENTITIES
|
||||||
|
|
||||||
# Test init all covers should be open
|
# Test init all covers should be open
|
||||||
assert is_open(hass, ent1)
|
assert is_open(hass, ent1)
|
||||||
@ -42,6 +43,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) -
|
|||||||
assert is_open(hass, ent3)
|
assert is_open(hass, ent3)
|
||||||
assert is_open(hass, ent4)
|
assert is_open(hass, ent4)
|
||||||
assert is_open(hass, ent5)
|
assert is_open(hass, ent5)
|
||||||
|
assert is_open(hass, ent6)
|
||||||
|
|
||||||
# call basic toggle services
|
# call basic toggle services
|
||||||
await call_service(hass, SERVICE_TOGGLE, ent1)
|
await call_service(hass, SERVICE_TOGGLE, ent1)
|
||||||
@ -49,13 +51,15 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) -
|
|||||||
await call_service(hass, SERVICE_TOGGLE, ent3)
|
await call_service(hass, SERVICE_TOGGLE, ent3)
|
||||||
await call_service(hass, SERVICE_TOGGLE, ent4)
|
await call_service(hass, SERVICE_TOGGLE, ent4)
|
||||||
await call_service(hass, SERVICE_TOGGLE, ent5)
|
await call_service(hass, SERVICE_TOGGLE, ent5)
|
||||||
|
await call_service(hass, SERVICE_TOGGLE, ent6)
|
||||||
|
|
||||||
# entities without stop should be closed and with stop should be closing
|
# entities should be either closed or closing, depending on if they report transitional states
|
||||||
assert is_closed(hass, ent1)
|
assert is_closed(hass, ent1)
|
||||||
assert is_closing(hass, ent2)
|
assert is_closing(hass, ent2)
|
||||||
assert is_closed(hass, ent3)
|
assert is_closed(hass, ent3)
|
||||||
assert is_closed(hass, ent4)
|
assert is_closed(hass, ent4)
|
||||||
assert is_closing(hass, ent5)
|
assert is_closing(hass, ent5)
|
||||||
|
assert is_closing(hass, ent6)
|
||||||
|
|
||||||
# call basic toggle services and set different cover position states
|
# call basic toggle services and set different cover position states
|
||||||
await call_service(hass, SERVICE_TOGGLE, ent1)
|
await call_service(hass, SERVICE_TOGGLE, ent1)
|
||||||
@ -65,6 +69,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) -
|
|||||||
await call_service(hass, SERVICE_TOGGLE, ent4)
|
await call_service(hass, SERVICE_TOGGLE, ent4)
|
||||||
set_cover_position(ent5, 15)
|
set_cover_position(ent5, 15)
|
||||||
await call_service(hass, SERVICE_TOGGLE, ent5)
|
await call_service(hass, SERVICE_TOGGLE, ent5)
|
||||||
|
await call_service(hass, SERVICE_TOGGLE, ent6)
|
||||||
|
|
||||||
# entities should be in correct state depending on the SUPPORT_STOP feature and cover position
|
# entities should be in correct state depending on the SUPPORT_STOP feature and cover position
|
||||||
assert is_open(hass, ent1)
|
assert is_open(hass, ent1)
|
||||||
@ -72,6 +77,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) -
|
|||||||
assert is_open(hass, ent3)
|
assert is_open(hass, ent3)
|
||||||
assert is_open(hass, ent4)
|
assert is_open(hass, ent4)
|
||||||
assert is_open(hass, ent5)
|
assert is_open(hass, ent5)
|
||||||
|
assert is_opening(hass, ent6)
|
||||||
|
|
||||||
# call basic toggle services
|
# call basic toggle services
|
||||||
await call_service(hass, SERVICE_TOGGLE, ent1)
|
await call_service(hass, SERVICE_TOGGLE, ent1)
|
||||||
@ -79,6 +85,7 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) -
|
|||||||
await call_service(hass, SERVICE_TOGGLE, ent3)
|
await call_service(hass, SERVICE_TOGGLE, ent3)
|
||||||
await call_service(hass, SERVICE_TOGGLE, ent4)
|
await call_service(hass, SERVICE_TOGGLE, ent4)
|
||||||
await call_service(hass, SERVICE_TOGGLE, ent5)
|
await call_service(hass, SERVICE_TOGGLE, ent5)
|
||||||
|
await call_service(hass, SERVICE_TOGGLE, ent6)
|
||||||
|
|
||||||
# entities should be in correct state depending on the SUPPORT_STOP feature and cover position
|
# entities should be in correct state depending on the SUPPORT_STOP feature and cover position
|
||||||
assert is_closed(hass, ent1)
|
assert is_closed(hass, ent1)
|
||||||
@ -86,6 +93,12 @@ async def test_services(hass: HomeAssistant, enable_custom_integrations: None) -
|
|||||||
assert is_closed(hass, ent3)
|
assert is_closed(hass, ent3)
|
||||||
assert is_closed(hass, ent4)
|
assert is_closed(hass, ent4)
|
||||||
assert is_opening(hass, ent5)
|
assert is_opening(hass, ent5)
|
||||||
|
assert is_closing(hass, ent6)
|
||||||
|
|
||||||
|
# Without STOP but still reports opening/closing has a 4th possible toggle state
|
||||||
|
set_state(ent6, STATE_CLOSED)
|
||||||
|
await call_service(hass, SERVICE_TOGGLE, ent6)
|
||||||
|
assert is_opening(hass, ent6)
|
||||||
|
|
||||||
|
|
||||||
def call_service(hass, service, ent):
|
def call_service(hass, service, ent):
|
||||||
@ -100,6 +113,11 @@ def set_cover_position(ent, position) -> None:
|
|||||||
ent._values["current_cover_position"] = position
|
ent._values["current_cover_position"] = position
|
||||||
|
|
||||||
|
|
||||||
|
def set_state(ent, state) -> None:
|
||||||
|
"""Set the state of a cover."""
|
||||||
|
ent._values["state"] = state
|
||||||
|
|
||||||
|
|
||||||
def is_open(hass, ent):
|
def is_open(hass, ent):
|
||||||
"""Return if the cover is closed based on the statemachine."""
|
"""Return if the cover is closed based on the statemachine."""
|
||||||
return hass.states.is_state(ent.entity_id, STATE_OPEN)
|
return hass.states.is_state(ent.entity_id, STATE_OPEN)
|
||||||
|
@ -227,3 +227,88 @@ async def test_no_next_event(
|
|||||||
assert state is not None
|
assert state is not None
|
||||||
assert state.state == "off"
|
assert state.state == "off"
|
||||||
assert state.attributes == {"friendly_name": "Germany"}
|
assert state.attributes == {"friendly_name": "Germany"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_language_not_exist(
|
||||||
|
hass: HomeAssistant, freezer: FrozenDateTimeFactory
|
||||||
|
) -> None:
|
||||||
|
"""Test when language doesn't exist it will fallback to country default language."""
|
||||||
|
|
||||||
|
hass.config.language = "nb" # Norweigan language "Norks bokmål"
|
||||||
|
hass.config.country = "NO"
|
||||||
|
|
||||||
|
freezer.move_to(datetime(2023, 1, 1, 12, tzinfo=dt_util.UTC))
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={CONF_COUNTRY: "NO"},
|
||||||
|
title="Norge",
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get("calendar.norge")
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == "on"
|
||||||
|
assert state.attributes == {
|
||||||
|
"friendly_name": "Norge",
|
||||||
|
"all_day": True,
|
||||||
|
"description": "",
|
||||||
|
"end_time": "2023-01-02 00:00:00",
|
||||||
|
"location": "Norge",
|
||||||
|
"message": "Første nyttårsdag",
|
||||||
|
"start_time": "2023-01-01 00:00:00",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await hass.services.async_call(
|
||||||
|
CALENDAR_DOMAIN,
|
||||||
|
SERVICE_GET_EVENTS,
|
||||||
|
{
|
||||||
|
"entity_id": "calendar.norge",
|
||||||
|
"end_date_time": dt_util.now(),
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
return_response=True,
|
||||||
|
)
|
||||||
|
assert response == {
|
||||||
|
"calendar.norge": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"start": "2023-01-01",
|
||||||
|
"end": "2023-01-02",
|
||||||
|
"summary": "Første nyttårsdag",
|
||||||
|
"location": "Norge",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test with English as exist as optional language for Norway
|
||||||
|
hass.config.language = "en"
|
||||||
|
hass.config.country = "NO"
|
||||||
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
response = await hass.services.async_call(
|
||||||
|
CALENDAR_DOMAIN,
|
||||||
|
SERVICE_GET_EVENTS,
|
||||||
|
{
|
||||||
|
"entity_id": "calendar.norge",
|
||||||
|
"end_date_time": dt_util.now(),
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
return_response=True,
|
||||||
|
)
|
||||||
|
assert response == {
|
||||||
|
"calendar.norge": {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"start": "2023-01-01",
|
||||||
|
"end": "2023-01-02",
|
||||||
|
"summary": "New Year's Day",
|
||||||
|
"location": "Norge",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -150,7 +150,6 @@ async def test_remove_device_trigger(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(hass.data[DOMAIN].telegrams._jobs) == 1
|
|
||||||
await knx.receive_write("0/0/1", (0x03, 0x2F))
|
await knx.receive_write("0/0/1", (0x03, 0x2F))
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
assert calls.pop().data["catch_all"] == "telegram - 0/0/1"
|
assert calls.pop().data["catch_all"] == "telegram - 0/0/1"
|
||||||
@ -161,8 +160,6 @@ async def test_remove_device_trigger(
|
|||||||
{ATTR_ENTITY_ID: f"automation.{automation_name}"},
|
{ATTR_ENTITY_ID: f"automation.{automation_name}"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(hass.data[DOMAIN].telegrams._jobs) == 0
|
|
||||||
await knx.receive_write("0/0/1", (0x03, 0x2F))
|
await knx.receive_write("0/0/1", (0x03, 0x2F))
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ TEST_JAVA_STATUS_RESPONSE = JavaStatusResponse(
|
|||||||
version=JavaStatusVersion.build(TEST_JAVA_STATUS_RESPONSE_RAW["version"]),
|
version=JavaStatusVersion.build(TEST_JAVA_STATUS_RESPONSE_RAW["version"]),
|
||||||
motd=Motd.parse(TEST_JAVA_STATUS_RESPONSE_RAW["description"], bedrock=False),
|
motd=Motd.parse(TEST_JAVA_STATUS_RESPONSE_RAW["description"], bedrock=False),
|
||||||
icon=None,
|
icon=None,
|
||||||
|
enforces_secure_chat=False,
|
||||||
latency=5,
|
latency=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -115,6 +115,63 @@ async def test_controlling_state_via_topic(
|
|||||||
assert state.state == ""
|
assert state.state == ""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"hass_config",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
mqtt.DOMAIN: {
|
||||||
|
text.DOMAIN: {
|
||||||
|
"name": "test",
|
||||||
|
"state_topic": "state-topic",
|
||||||
|
"command_topic": "command-topic",
|
||||||
|
"min": 5,
|
||||||
|
"max": 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_forced_text_length(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test a text entity that only allows a fixed length."""
|
||||||
|
await mqtt_mock_entry()
|
||||||
|
|
||||||
|
state = hass.states.get("text.test")
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
assert not state.attributes.get(ATTR_ASSUMED_STATE)
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "state-topic", "12345")
|
||||||
|
state = hass.states.get("text.test")
|
||||||
|
assert state.state == "12345"
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
|
# Text too long
|
||||||
|
async_fire_mqtt_message(hass, "state-topic", "123456")
|
||||||
|
state = hass.states.get("text.test")
|
||||||
|
assert state.state == "12345"
|
||||||
|
assert (
|
||||||
|
"ValueError: Entity text.test provides state 123456 "
|
||||||
|
"which is too long (maximum length 5)" in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
|
# Text too short
|
||||||
|
async_fire_mqtt_message(hass, "state-topic", "1")
|
||||||
|
state = hass.states.get("text.test")
|
||||||
|
assert state.state == "12345"
|
||||||
|
assert (
|
||||||
|
"ValueError: Entity text.test provides state 1 "
|
||||||
|
"which is too short (minimum length 5)" in caplog.text
|
||||||
|
)
|
||||||
|
# Valid update
|
||||||
|
async_fire_mqtt_message(hass, "state-topic", "54321")
|
||||||
|
state = hass.states.get("text.test")
|
||||||
|
assert state.state == "54321"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"hass_config",
|
"hass_config",
|
||||||
[
|
[
|
||||||
@ -211,7 +268,7 @@ async def test_attribute_validation_max_greater_then_min(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test the validation of min and max configuration attributes."""
|
"""Test the validation of min and max configuration attributes."""
|
||||||
assert await mqtt_mock_entry()
|
assert await mqtt_mock_entry()
|
||||||
assert "text length min must be >= max" in caplog.text
|
assert "text length min must be <= max" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
85
tests/components/swiss_public_transport/test_init.py
Normal file
85
tests/components/swiss_public_transport/test_init.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
"""Test the swiss_public_transport config flow."""
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from homeassistant.components.swiss_public_transport.const import (
|
||||||
|
CONF_DESTINATION,
|
||||||
|
CONF_START,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
MOCK_DATA_STEP = {
|
||||||
|
CONF_START: "test_start",
|
||||||
|
CONF_DESTINATION: "test_destination",
|
||||||
|
}
|
||||||
|
|
||||||
|
CONNECTIONS = [
|
||||||
|
{
|
||||||
|
"departure": "2024-01-06T18:03:00+0100",
|
||||||
|
"number": 0,
|
||||||
|
"platform": 0,
|
||||||
|
"transfers": 0,
|
||||||
|
"duration": "10",
|
||||||
|
"delay": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"departure": "2024-01-06T18:04:00+0100",
|
||||||
|
"number": 1,
|
||||||
|
"platform": 1,
|
||||||
|
"transfers": 0,
|
||||||
|
"duration": "10",
|
||||||
|
"delay": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"departure": "2024-01-06T18:05:00+0100",
|
||||||
|
"number": 2,
|
||||||
|
"platform": 2,
|
||||||
|
"transfers": 0,
|
||||||
|
"duration": "10",
|
||||||
|
"delay": 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_migration_1_to_2(
|
||||||
|
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||||
|
) -> None:
|
||||||
|
"""Test successful setup."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.swiss_public_transport.OpendataTransport",
|
||||||
|
return_value=AsyncMock(),
|
||||||
|
) as mock:
|
||||||
|
mock().connections = CONNECTIONS
|
||||||
|
|
||||||
|
config_entry_faulty = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=MOCK_DATA_STEP,
|
||||||
|
title="MIGRATION_TEST",
|
||||||
|
minor_version=1,
|
||||||
|
)
|
||||||
|
config_entry_faulty.add_to_hass(hass)
|
||||||
|
|
||||||
|
# Setup the config entry
|
||||||
|
await hass.config_entries.async_setup(config_entry_faulty.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert entity_registry.async_is_registered(
|
||||||
|
entity_registry.entities.get_entity_id(
|
||||||
|
(Platform.SENSOR, DOMAIN, "test_start test_destination_departure")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check change in config entry
|
||||||
|
assert config_entry_faulty.minor_version == 2
|
||||||
|
assert config_entry_faulty.unique_id == "test_start test_destination"
|
||||||
|
|
||||||
|
# Check "None" is gone
|
||||||
|
assert not entity_registry.async_is_registered(
|
||||||
|
entity_registry.entities.get_entity_id(
|
||||||
|
(Platform.SENSOR, DOMAIN, "None_departure")
|
||||||
|
)
|
||||||
|
)
|
@ -26,6 +26,10 @@ DUMMY_DEVICE_ID1 = "a123bc"
|
|||||||
DUMMY_DEVICE_ID2 = "cafe12"
|
DUMMY_DEVICE_ID2 = "cafe12"
|
||||||
DUMMY_DEVICE_ID3 = "bada77"
|
DUMMY_DEVICE_ID3 = "bada77"
|
||||||
DUMMY_DEVICE_ID4 = "bbd164"
|
DUMMY_DEVICE_ID4 = "bbd164"
|
||||||
|
DUMMY_DEVICE_KEY1 = "18"
|
||||||
|
DUMMY_DEVICE_KEY2 = "01"
|
||||||
|
DUMMY_DEVICE_KEY3 = "12"
|
||||||
|
DUMMY_DEVICE_KEY4 = "07"
|
||||||
DUMMY_DEVICE_NAME1 = "Plug 23BC"
|
DUMMY_DEVICE_NAME1 = "Plug 23BC"
|
||||||
DUMMY_DEVICE_NAME2 = "Heater FE12"
|
DUMMY_DEVICE_NAME2 = "Heater FE12"
|
||||||
DUMMY_DEVICE_NAME3 = "Breeze AB39"
|
DUMMY_DEVICE_NAME3 = "Breeze AB39"
|
||||||
@ -67,6 +71,7 @@ DUMMY_PLUG_DEVICE = SwitcherPowerPlug(
|
|||||||
DeviceType.POWER_PLUG,
|
DeviceType.POWER_PLUG,
|
||||||
DeviceState.ON,
|
DeviceState.ON,
|
||||||
DUMMY_DEVICE_ID1,
|
DUMMY_DEVICE_ID1,
|
||||||
|
DUMMY_DEVICE_KEY1,
|
||||||
DUMMY_IP_ADDRESS1,
|
DUMMY_IP_ADDRESS1,
|
||||||
DUMMY_MAC_ADDRESS1,
|
DUMMY_MAC_ADDRESS1,
|
||||||
DUMMY_DEVICE_NAME1,
|
DUMMY_DEVICE_NAME1,
|
||||||
@ -78,6 +83,7 @@ DUMMY_WATER_HEATER_DEVICE = SwitcherWaterHeater(
|
|||||||
DeviceType.V4,
|
DeviceType.V4,
|
||||||
DeviceState.ON,
|
DeviceState.ON,
|
||||||
DUMMY_DEVICE_ID2,
|
DUMMY_DEVICE_ID2,
|
||||||
|
DUMMY_DEVICE_KEY2,
|
||||||
DUMMY_IP_ADDRESS2,
|
DUMMY_IP_ADDRESS2,
|
||||||
DUMMY_MAC_ADDRESS2,
|
DUMMY_MAC_ADDRESS2,
|
||||||
DUMMY_DEVICE_NAME2,
|
DUMMY_DEVICE_NAME2,
|
||||||
@ -91,6 +97,7 @@ DUMMY_SHUTTER_DEVICE = SwitcherShutter(
|
|||||||
DeviceType.RUNNER,
|
DeviceType.RUNNER,
|
||||||
DeviceState.ON,
|
DeviceState.ON,
|
||||||
DUMMY_DEVICE_ID4,
|
DUMMY_DEVICE_ID4,
|
||||||
|
DUMMY_DEVICE_KEY4,
|
||||||
DUMMY_IP_ADDRESS4,
|
DUMMY_IP_ADDRESS4,
|
||||||
DUMMY_MAC_ADDRESS4,
|
DUMMY_MAC_ADDRESS4,
|
||||||
DUMMY_DEVICE_NAME4,
|
DUMMY_DEVICE_NAME4,
|
||||||
@ -102,6 +109,7 @@ DUMMY_THERMOSTAT_DEVICE = SwitcherThermostat(
|
|||||||
DeviceType.BREEZE,
|
DeviceType.BREEZE,
|
||||||
DeviceState.ON,
|
DeviceState.ON,
|
||||||
DUMMY_DEVICE_ID3,
|
DUMMY_DEVICE_ID3,
|
||||||
|
DUMMY_DEVICE_KEY3,
|
||||||
DUMMY_IP_ADDRESS3,
|
DUMMY_IP_ADDRESS3,
|
||||||
DUMMY_MAC_ADDRESS3,
|
DUMMY_MAC_ADDRESS3,
|
||||||
DUMMY_DEVICE_NAME3,
|
DUMMY_DEVICE_NAME3,
|
||||||
|
@ -25,6 +25,7 @@ async def test_diagnostics(
|
|||||||
{
|
{
|
||||||
"auto_shutdown": "02:00:00",
|
"auto_shutdown": "02:00:00",
|
||||||
"device_id": REDACTED,
|
"device_id": REDACTED,
|
||||||
|
"device_key": REDACTED,
|
||||||
"device_state": {
|
"device_state": {
|
||||||
"__type": "<enum 'DeviceState'>",
|
"__type": "<enum 'DeviceState'>",
|
||||||
"repr": "<DeviceState.ON: ('01', 'on')>",
|
"repr": "<DeviceState.ON: ('01', 'on')>",
|
||||||
|
26
tests/components/tado/fixtures/mobile_devices.json
Normal file
26
tests/components/tado/fixtures/mobile_devices.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Home",
|
||||||
|
"id": 123456,
|
||||||
|
"settings": {
|
||||||
|
"geoTrackingEnabled": false,
|
||||||
|
"specialOffersEnabled": false,
|
||||||
|
"onDemandLogRetrievalEnabled": false,
|
||||||
|
"pushNotifications": {
|
||||||
|
"lowBatteryReminder": true,
|
||||||
|
"awayModeReminder": true,
|
||||||
|
"homeModeReminder": true,
|
||||||
|
"openWindowReminder": true,
|
||||||
|
"energySavingsReportReminder": true,
|
||||||
|
"incidentDetection": true,
|
||||||
|
"energyIqReminder": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deviceMetadata": {
|
||||||
|
"platform": "Android",
|
||||||
|
"osVersion": "14",
|
||||||
|
"model": "Samsung",
|
||||||
|
"locale": "nl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -17,6 +17,7 @@ async def async_init_integration(
|
|||||||
|
|
||||||
token_fixture = "tado/token.json"
|
token_fixture = "tado/token.json"
|
||||||
devices_fixture = "tado/devices.json"
|
devices_fixture = "tado/devices.json"
|
||||||
|
mobile_devices_fixture = "tado/mobile_devices.json"
|
||||||
me_fixture = "tado/me.json"
|
me_fixture = "tado/me.json"
|
||||||
weather_fixture = "tado/weather.json"
|
weather_fixture = "tado/weather.json"
|
||||||
home_state_fixture = "tado/home_state.json"
|
home_state_fixture = "tado/home_state.json"
|
||||||
@ -70,6 +71,10 @@ async def async_init_integration(
|
|||||||
"https://my.tado.com/api/v2/homes/1/devices",
|
"https://my.tado.com/api/v2/homes/1/devices",
|
||||||
text=load_fixture(devices_fixture),
|
text=load_fixture(devices_fixture),
|
||||||
)
|
)
|
||||||
|
m.get(
|
||||||
|
"https://my.tado.com/api/v2/homes/1/mobileDevices",
|
||||||
|
text=load_fixture(mobile_devices_fixture),
|
||||||
|
)
|
||||||
m.get(
|
m.get(
|
||||||
"https://my.tado.com/api/v2/devices/WR1/",
|
"https://my.tado.com/api/v2/devices/WR1/",
|
||||||
text=load_fixture(device_wr1_fixture),
|
text=load_fixture(device_wr1_fixture),
|
||||||
|
@ -264,6 +264,26 @@ async def test_color_temp_light(
|
|||||||
bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None)
|
bulb.set_color_temp.assert_called_with(6666, brightness=None, transition=None)
|
||||||
bulb.set_color_temp.reset_mock()
|
bulb.set_color_temp.reset_mock()
|
||||||
|
|
||||||
|
# Verify color temp is clamped to the valid range
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 20000},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
bulb.set_color_temp.assert_called_with(9000, brightness=None, transition=None)
|
||||||
|
bulb.set_color_temp.reset_mock()
|
||||||
|
|
||||||
|
# Verify color temp is clamped to the valid range
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP_KELVIN: 1},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
bulb.set_color_temp.assert_called_with(4000, brightness=None, transition=None)
|
||||||
|
bulb.set_color_temp.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
async def test_brightness_only_light(hass: HomeAssistant) -> None:
|
async def test_brightness_only_light(hass: HomeAssistant) -> None:
|
||||||
"""Test a light."""
|
"""Test a light."""
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user