mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +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
|
||||
/homeassistant/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
|
||||
/tests/components/zwave_js/ @home-assistant/z-wave
|
||||
/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.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__)
|
||||
|
||||
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_VERSION = 1
|
||||
STORAGE_EXPIRE_TIME = "expire_time"
|
||||
STORAGE_ACCESS_TOKEN = "access_token"
|
||||
STORAGE_REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
|
||||
class Auth:
|
||||
@ -56,7 +57,7 @@ class Auth:
|
||||
}
|
||||
_LOGGER.debug(
|
||||
"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)
|
||||
@ -133,7 +134,7 @@ class Auth:
|
||||
return None
|
||||
|
||||
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"]
|
||||
refresh_token: str = response_json["refresh_token"]
|
||||
|
@ -1112,13 +1112,17 @@ class AlexaThermostatController(AlexaCapability):
|
||||
"""Return what properties this entity supports."""
|
||||
properties = [{"name": "thermostatMode"}]
|
||||
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"})
|
||||
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
|
||||
|
||||
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
|
||||
PRESET_MODE_NA = "-"
|
||||
|
||||
STORAGE_ACCESS_TOKEN = "access_token"
|
||||
STORAGE_REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
|
||||
class Cause:
|
||||
"""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.
|
||||
"""
|
||||
auth_code: str = directive.payload["grant"]["code"]
|
||||
_LOGGER.debug("AcceptGrant code: %s", auth_code)
|
||||
|
||||
if config.supports_auth:
|
||||
await config.async_accept_grant(auth_code)
|
||||
|
@ -25,6 +25,7 @@ from .const import (
|
||||
CONF_LOCALE,
|
||||
EVENT_ALEXA_SMART_HOME,
|
||||
)
|
||||
from .diagnostics import async_redact_auth_data
|
||||
from .errors import AlexaBridgeUnreachableError, AlexaError
|
||||
from .handlers import HANDLERS
|
||||
from .state_report import AlexaDirective
|
||||
@ -149,12 +150,21 @@ class SmartHomeView(HomeAssistantView):
|
||||
user: User = request["hass_user"]
|
||||
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(
|
||||
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)
|
||||
|
||||
|
||||
|
@ -34,6 +34,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
Cause,
|
||||
)
|
||||
from .diagnostics import async_redact_auth_data
|
||||
from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
|
||||
from .errors import AlexaInvalidEndpointError, NoTokenAvailable, RequireRelink
|
||||
|
||||
@ -43,6 +44,8 @@ if TYPE_CHECKING:
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
TO_REDACT = {"correlationToken", "token"}
|
||||
|
||||
|
||||
class AlexaDirective:
|
||||
"""An incoming Alexa directive."""
|
||||
@ -379,7 +382,9 @@ async def async_send_changereport_message(
|
||||
response_text = await response.text()
|
||||
|
||||
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)
|
||||
|
||||
if response.status == HTTPStatus.ACCEPTED:
|
||||
@ -533,7 +538,9 @@ async def async_send_doorbell_event_message(
|
||||
response_text = await response.text()
|
||||
|
||||
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)
|
||||
|
||||
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,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||
"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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = 30
|
||||
SCAN_INTERVAL = 300
|
||||
|
||||
|
||||
class BlinkUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/blink",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["blinkpy"],
|
||||
"requirements": ["blinkpy==0.22.4"]
|
||||
"requirements": ["blinkpy==0.22.5"]
|
||||
}
|
||||
|
@ -17,9 +17,9 @@
|
||||
"bleak==0.21.1",
|
||||
"bleak-retry-connector==3.4.0",
|
||||
"bluetooth-adapters==0.16.2",
|
||||
"bluetooth-auto-recovery==1.2.3",
|
||||
"bluetooth-auto-recovery==1.3.0",
|
||||
"bluetooth-data-tools==1.19.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:
|
||||
"""Discover platforms."""
|
||||
"""Handle cloud started after login."""
|
||||
nonlocal loaded
|
||||
|
||||
# Prevent multiple discovery
|
||||
@ -299,14 +299,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return
|
||||
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"})
|
||||
|
||||
async def _on_connect() -> None:
|
||||
@ -335,6 +327,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
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(
|
||||
hass=hass,
|
||||
delay=timedelta(hours=STARTUP_REPAIR_DELAY),
|
||||
|
@ -72,6 +72,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_reauth_entry: ConfigEntry | None
|
||||
_reauth_host: str
|
||||
_reauth_port: int
|
||||
_reauth_type: str
|
||||
|
||||
async def async_step_user(
|
||||
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_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}
|
||||
return await self.async_step_reauth_confirm()
|
||||
@ -127,6 +129,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
{
|
||||
CONF_HOST: self._reauth_host,
|
||||
CONF_PORT: self._reauth_port,
|
||||
CONF_TYPE: self._reauth_type,
|
||||
}
|
||||
| user_input,
|
||||
)
|
||||
@ -144,6 +147,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_HOST: self._reauth_host,
|
||||
CONF_PORT: self._reauth_port,
|
||||
CONF_PIN: user_input[CONF_PIN],
|
||||
CONF_TYPE: self._reauth_type,
|
||||
},
|
||||
)
|
||||
self.hass.async_create_task(
|
||||
|
@ -81,15 +81,11 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
try:
|
||||
await self.api.login()
|
||||
return await self._async_update_system_data()
|
||||
except exceptions.CannotConnect as err:
|
||||
_LOGGER.warning("Connection error for %s", self._host)
|
||||
await self.api.close()
|
||||
raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
|
||||
except (exceptions.CannotConnect, exceptions.CannotRetrieveData) as err:
|
||||
raise UpdateFailed(repr(err)) from err
|
||||
except exceptions.CannotAuthenticate:
|
||||
raise ConfigEntryAuthFailed
|
||||
|
||||
return {}
|
||||
|
||||
@abstractmethod
|
||||
async def _async_update_system_data(self) -> dict[str, Any]:
|
||||
"""Class method for updating data."""
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/comelit",
|
||||
"iot_class": "local_polling",
|
||||
"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(
|
||||
self, fns: dict[str, 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
|
||||
):
|
||||
return fns["stop"]
|
||||
|
@ -479,10 +479,20 @@ class EnvoyInverterEntity(EnvoySensorBaseEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> datetime.datetime | float:
|
||||
def native_value(self) -> datetime.datetime | float | None:
|
||||
"""Return the state of the sensor."""
|
||||
inverters = self.data.inverters
|
||||
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])
|
||||
|
||||
|
||||
|
@ -497,7 +497,6 @@ class EvoBroker:
|
||||
|
||||
session_id = get_session_id(self.client_v1)
|
||||
|
||||
self.temps = {} # these are now stale, will fall back to v2 temps
|
||||
try:
|
||||
temps = await self.client_v1.get_temperatures()
|
||||
|
||||
@ -523,6 +522,11 @@ class EvoBroker:
|
||||
),
|
||||
err,
|
||||
)
|
||||
self.temps = {} # high-precision temps now considered stale
|
||||
|
||||
except Exception:
|
||||
self.temps = {} # high-precision temps now considered stale
|
||||
raise
|
||||
|
||||
else:
|
||||
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
|
||||
|
||||
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_device.temperature
|
||||
|
||||
|
@ -118,7 +118,6 @@ class FAABinarySensor(CoordinatorEntity[FAADataUpdateCoordinator], BinarySensorE
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
_id = coordinator.data.code
|
||||
self._attr_name = f"{_id} {description.name}"
|
||||
self._attr_unique_id = f"{_id}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, _id)},
|
||||
|
@ -27,6 +27,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
MAX_TEMP,
|
||||
MIN_TEMP,
|
||||
PRESET_TO_VENTILATION_MODE_MAP,
|
||||
VENTILATION_TO_PRESET_MODE_MAP,
|
||||
)
|
||||
@ -67,6 +69,8 @@ class FlexitClimateEntity(ClimateEntity):
|
||||
|
||||
_attr_target_temperature_step = PRECISION_HALVES
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_max_temp = MAX_TEMP
|
||||
_attr_min_temp = MIN_TEMP
|
||||
|
||||
def __init__(self, device: FlexitBACnet) -> None:
|
||||
"""Initialize the unit."""
|
||||
|
@ -15,6 +15,9 @@ from homeassistant.components.climate import (
|
||||
|
||||
DOMAIN = "flexit_bacnet"
|
||||
|
||||
MAX_TEMP = 30
|
||||
MIN_TEMP = 10
|
||||
|
||||
VENTILATION_TO_PRESET_MODE_MAP = {
|
||||
VENTILATION_MODE_STOP: PRESET_NONE,
|
||||
VENTILATION_MODE_AWAY: PRESET_AWAY,
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"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
|
||||
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(
|
||||
[
|
||||
|
@ -118,7 +118,7 @@ async def async_setup_platform(
|
||||
|
||||
mode = get_ip_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.")
|
||||
|
||||
unique_id = f"kef-{mac}"
|
||||
|
@ -82,6 +82,9 @@ DATA_HASS_CONFIG: Final = "knx_hass_config"
|
||||
ATTR_COUNTER: Final = "counter"
|
||||
ATTR_SOURCE: Final = "source"
|
||||
|
||||
# dispatcher signal for KNX interface device triggers
|
||||
SIGNAL_KNX_TELEGRAM_DICT: Final = "knx_telegram_dict"
|
||||
|
||||
AsyncMessageCallbackType = Callable[[Telegram], Awaitable[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.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import KNXModule
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, SIGNAL_KNX_TELEGRAM_DICT
|
||||
from .project import KNXProject
|
||||
from .schema import ga_list_validator
|
||||
from .telegrams import TelegramDict
|
||||
@ -87,7 +88,6 @@ async def async_attach_trigger(
|
||||
trigger_data = trigger_info["trigger_data"]
|
||||
dst_addresses: list[str] = config.get(EXTRA_FIELD_DESTINATION, [])
|
||||
job = HassJob(action, f"KNX device trigger {trigger_info}")
|
||||
knx: KNXModule = hass.data[DOMAIN]
|
||||
|
||||
@callback
|
||||
def async_call_trigger_action(telegram: TelegramDict) -> None:
|
||||
@ -99,6 +99,8 @@ async def async_attach_trigger(
|
||||
{"trigger": {**trigger_data, **telegram}},
|
||||
)
|
||||
|
||||
return knx.telegrams.async_listen_telegram(
|
||||
async_call_trigger_action, name="KNX device trigger call"
|
||||
return async_dispatcher_connect(
|
||||
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 homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.storage import Store
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, SIGNAL_KNX_TELEGRAM_DICT
|
||||
from .project import KNXProject
|
||||
|
||||
STORAGE_VERSION: Final = 1
|
||||
@ -87,6 +88,7 @@ class Telegrams:
|
||||
"""Handle incoming and outgoing telegrams from xknx."""
|
||||
telegram_dict = self.telegram_to_dict(telegram)
|
||||
self.recent_telegrams.append(telegram_dict)
|
||||
async_dispatcher_send(self.hass, SIGNAL_KNX_TELEGRAM_DICT, telegram_dict)
|
||||
for job in self._jobs:
|
||||
self.hass.async_run_hass_job(job, telegram_dict)
|
||||
|
||||
|
@ -2,7 +2,11 @@
|
||||
|
||||
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 homeassistant.components import bluetooth
|
||||
@ -24,6 +28,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up LD2410 BLE from a config entry."""
|
||||
address: str = entry.data[CONF_ADDRESS]
|
||||
|
||||
await close_stale_connections_by_address(address)
|
||||
|
||||
ble_device = bluetooth.async_ble_device_from_address(
|
||||
hass, address.upper(), True
|
||||
) 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}"
|
||||
)
|
||||
|
||||
await close_stale_connections(ble_device)
|
||||
|
||||
ld2410_ble = LD2410BLE(ble_device)
|
||||
|
||||
coordinator = LD2410BLECoordinator(hass, ld2410_ble)
|
||||
|
@ -3,7 +3,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"station_id": "Sensor ID",
|
||||
"sensor_id": "Sensor ID",
|
||||
"show_on_map": "Show on map"
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Coroutine
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
@ -267,11 +269,11 @@ class MicrosoftFace:
|
||||
"""Store group/person data and IDs."""
|
||||
return self._store
|
||||
|
||||
async def update_store(self):
|
||||
async def update_store(self) -> None:
|
||||
"""Load all group/person data into local store."""
|
||||
groups = await self.call_api("get", "persongroups")
|
||||
|
||||
remove_tasks = []
|
||||
remove_tasks: list[Coroutine[Any, Any, None]] = []
|
||||
new_entities = []
|
||||
for group in groups:
|
||||
g_id = group["personGroupId"]
|
||||
@ -293,7 +295,7 @@ class MicrosoftFace:
|
||||
self._store[g_id][person["name"]] = person["personId"]
|
||||
|
||||
if remove_tasks:
|
||||
await asyncio.gather(remove_tasks)
|
||||
await asyncio.gather(*remove_tasks)
|
||||
await self._component.async_add_entities(new_entities)
|
||||
|
||||
async def call_api(self, method, function, data=None, binary=False, params=None):
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["dnspython", "mcstatus"],
|
||||
"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:
|
||||
"""Validate that the text length configuration is valid, throws if it isn't."""
|
||||
if config[CONF_MIN] >= config[CONF_MAX]:
|
||||
raise vol.Invalid("text length min must be >= max")
|
||||
if config[CONF_MIN] > config[CONF_MAX]:
|
||||
raise vol.Invalid("text length min must be <= max")
|
||||
if config[CONF_MAX] > 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",
|
||||
"iot_class": "local_push",
|
||||
"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,
|
||||
)
|
||||
try:
|
||||
if not await coordinator.gateway.async_set_color_lights(color_num):
|
||||
raise HomeAssistantError(
|
||||
f"Failed to call service '{SERVICE_SET_COLOR_MODE}'"
|
||||
)
|
||||
await coordinator.gateway.async_set_color_lights(color_num)
|
||||
# Debounced refresh to catch any secondary
|
||||
# changes in the device
|
||||
await coordinator.async_request_refresh()
|
||||
|
@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioshelly"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioshelly==7.0.0"],
|
||||
"requirements": ["aioshelly==7.1.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
@ -36,6 +36,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
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 .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator
|
||||
@ -969,7 +970,7 @@ def _build_block_description(entry: RegistryEntry) -> BlockSensorDescription:
|
||||
name="",
|
||||
icon=entry.original_icon,
|
||||
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()
|
||||
}
|
||||
|
||||
# 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(
|
||||
hass: HomeAssistant,
|
||||
@ -423,7 +427,19 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
||||
if context and (self._playlist is None or self._playlist["uri"] != uri):
|
||||
self._playlist = None
|
||||
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")
|
||||
if device is not None:
|
||||
|
@ -10,6 +10,7 @@ from opendata_transport.exceptions import (
|
||||
from homeassistant import config_entries, core
|
||||
from homeassistant.const import Platform
|
||||
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 .const import CONF_DESTINATION, CONF_START, DOMAIN
|
||||
@ -65,3 +66,51 @@ async def async_unload_entry(
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
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."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@ -59,6 +60,9 @@ class SwissPublicTransportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unknown error")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(
|
||||
f"{user_input[CONF_START]} {user_input[CONF_DESTINATION]}"
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=f"{user_input[CONF_START]} {user_input[CONF_DESTINATION]}",
|
||||
data=user_input,
|
||||
@ -98,6 +102,9 @@ class SwissPublicTransportConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
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(
|
||||
title=import_input[CONF_NAME],
|
||||
data=import_input,
|
||||
|
@ -122,15 +122,25 @@ class SwissPublicTransportSensor(
|
||||
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
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""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 = {
|
||||
key: value
|
||||
for key, value in self.coordinator.data.items()
|
||||
if key not in {"departure"}
|
||||
}
|
||||
return super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def native_value(self) -> str:
|
||||
|
@ -89,8 +89,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
# New device - create device
|
||||
_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_key,
|
||||
device.name,
|
||||
device.device_type.value,
|
||||
device.device_type.hex_rep,
|
||||
|
@ -142,7 +142,9 @@ class SwitcherThermostatButtonEntity(
|
||||
|
||||
try:
|
||||
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:
|
||||
response = await self.entity_description.press_fn(swapi, self._remote)
|
||||
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
||||
|
@ -162,7 +162,9 @@ class SwitcherClimateEntity(
|
||||
|
||||
try:
|
||||
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:
|
||||
response = await swapi.control_breeze_device(self._remote, **kwargs)
|
||||
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
||||
|
@ -98,7 +98,9 @@ class SwitcherCoverEntity(
|
||||
|
||||
try:
|
||||
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:
|
||||
response = await getattr(swapi, api)(*args)
|
||||
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
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(
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioswitcher"],
|
||||
"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:
|
||||
"""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
|
||||
error = None
|
||||
|
||||
try:
|
||||
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:
|
||||
response = await getattr(swapi, api)(*args)
|
||||
except (asyncio.TimeoutError, OSError, RuntimeError) as err:
|
||||
|
@ -405,7 +405,7 @@ async def async_setup_entry(
|
||||
is_enabled = check_legacy_resource(
|
||||
f"{_type}_{argument}", legacy_resources
|
||||
)
|
||||
loaded_resources.add(f"{_type}_{argument}")
|
||||
loaded_resources.add(f"{_type}_{slugify(argument)}")
|
||||
entities.append(
|
||||
SystemMonitorSensor(
|
||||
sensor_registry,
|
||||
@ -425,7 +425,7 @@ async def async_setup_entry(
|
||||
is_enabled = check_legacy_resource(
|
||||
f"{_type}_{argument}", legacy_resources
|
||||
)
|
||||
loaded_resources.add(f"{_type}_{argument}")
|
||||
loaded_resources.add(f"{_type}_{slugify(argument)}")
|
||||
entities.append(
|
||||
SystemMonitorSensor(
|
||||
sensor_registry,
|
||||
@ -449,7 +449,7 @@ async def async_setup_entry(
|
||||
sensor_registry[(_type, argument)] = SensorData(
|
||||
argument, None, None, None, None
|
||||
)
|
||||
loaded_resources.add(f"{_type}_{argument}")
|
||||
loaded_resources.add(f"{_type}_{slugify(argument)}")
|
||||
entities.append(
|
||||
SystemMonitorSensor(
|
||||
sensor_registry,
|
||||
@ -478,10 +478,13 @@ async def async_setup_entry(
|
||||
# of mount points automatically discovered
|
||||
for resource in legacy_resources:
|
||||
if resource.startswith("disk_"):
|
||||
check_resource = slugify(resource)
|
||||
_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("_")
|
||||
_type = resource[:split_index]
|
||||
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
|
||||
)
|
||||
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 != "":
|
||||
disks.add(part.mountpoint)
|
||||
_LOGGER.debug("Adding disks: %s", ", ".join(disks))
|
||||
|
@ -186,12 +186,13 @@ class TadoConnector:
|
||||
|
||||
def get_mobile_devices(self):
|
||||
"""Return the Tado mobile devices."""
|
||||
return self.tado.getMobileDevices()
|
||||
return self.tado.get_mobile_devices()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update the registered zones."""
|
||||
self.update_devices()
|
||||
self.update_mobile_devices()
|
||||
self.update_zones()
|
||||
self.update_home()
|
||||
|
||||
@ -203,17 +204,31 @@ class TadoConnector:
|
||||
_LOGGER.error("Unable to connect to Tado while updating mobile devices")
|
||||
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:
|
||||
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(
|
||||
self.hass,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(self.home_id),
|
||||
)
|
||||
|
||||
def update_devices(self):
|
||||
@ -224,6 +239,20 @@ class TadoConnector:
|
||||
_LOGGER.error("Unable to connect to Tado while updating devices")
|
||||
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:
|
||||
device_short_serial_no = device["shortSerialNo"]
|
||||
_LOGGER.debug("Updating device %s", device_short_serial_no)
|
||||
|
@ -179,7 +179,7 @@ TADO_TO_HA_SWING_MODE_MAP = {
|
||||
DOMAIN = "tado"
|
||||
|
||||
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"
|
||||
|
||||
DEFAULT_NAME = "Tado"
|
||||
|
@ -2,7 +2,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
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.typing import ConfigType
|
||||
|
||||
from . import TadoConnector
|
||||
from .const import CONF_HOME_ID, DATA, DOMAIN, SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -90,7 +90,7 @@ async def async_setup_entry(
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(tado.home_id),
|
||||
update_devices,
|
||||
)
|
||||
)
|
||||
@ -99,12 +99,12 @@ async def async_setup_entry(
|
||||
@callback
|
||||
def add_tracked_entities(
|
||||
hass: HomeAssistant,
|
||||
tado: Any,
|
||||
tado: TadoConnector,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
tracked: set[str],
|
||||
) -> None:
|
||||
"""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 = []
|
||||
for device_key, device in tado.data["mobile_device"].items():
|
||||
if device_key in tracked:
|
||||
@ -128,7 +128,7 @@ class TadoDeviceTrackerEntity(TrackerEntity):
|
||||
self,
|
||||
device_id: str,
|
||||
device_name: str,
|
||||
tado: Any,
|
||||
tado: TadoConnector,
|
||||
) -> None:
|
||||
"""Initialize a Tado Device Tracker entity."""
|
||||
super().__init__()
|
||||
@ -169,7 +169,7 @@ class TadoDeviceTrackerEntity(TrackerEntity):
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED,
|
||||
SIGNAL_TADO_MOBILE_DEVICE_UPDATE_RECEIVED.format(self._tado.home_id),
|
||||
self.on_demand_update,
|
||||
)
|
||||
)
|
||||
|
@ -121,5 +121,6 @@ class TailwindDoorCoverEntity(TailwindDoorEntity, CoverEntity):
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
) from exc
|
||||
self._attr_is_closing = False
|
||||
finally:
|
||||
self._attr_is_closing = False
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
@ -220,6 +220,26 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
||||
hue, sat = tuple(int(val) for val in hs_color)
|
||||
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(
|
||||
self, brightness: int | None, transition: int | None
|
||||
) -> None:
|
||||
@ -234,10 +254,8 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
|
||||
"""Turn the light on."""
|
||||
brightness, transition = self._async_extract_brightness_transition(**kwargs)
|
||||
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||
await self.device.set_color_temp(
|
||||
int(kwargs[ATTR_COLOR_TEMP_KELVIN]),
|
||||
brightness=brightness,
|
||||
transition=transition,
|
||||
await self._async_set_color_temp(
|
||||
kwargs[ATTR_COLOR_TEMP_KELVIN], brightness, transition
|
||||
)
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
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
|
||||
# before we can set a color temp
|
||||
await self.device.set_hsv(0, 0, brightness)
|
||||
await self.device.set_color_temp(
|
||||
int(kwargs[ATTR_COLOR_TEMP_KELVIN]),
|
||||
brightness=brightness,
|
||||
transition=transition,
|
||||
await self._async_set_color_temp(
|
||||
kwargs[ATTR_COLOR_TEMP_KELVIN], brightness, transition
|
||||
)
|
||||
elif ATTR_HS_COLOR in kwargs:
|
||||
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]
|
||||
if tasks:
|
||||
await asyncio.wait(tasks)
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
for conf in domain_config:
|
||||
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
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0,
|
||||
step="any",
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
),
|
||||
|
@ -173,15 +173,6 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||
ufp_value="is_vehicle_detection_on",
|
||||
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(
|
||||
key="smart_package",
|
||||
name="Detections: Package",
|
||||
@ -202,13 +193,22 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||
),
|
||||
ProtectBinaryEntityDescription(
|
||||
key="smart_smoke",
|
||||
name="Detections: Smoke/CO",
|
||||
name="Detections: Smoke",
|
||||
icon="mdi:fire",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ufp_required_field="can_detect_smoke",
|
||||
ufp_value="is_smoke_detection_on",
|
||||
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, ...] = (
|
||||
@ -342,7 +342,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
key="motion",
|
||||
name="Motion",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
ufp_value="is_motion_detected",
|
||||
ufp_value="is_motion_currently_detected",
|
||||
ufp_enabled="is_motion_detection_on",
|
||||
ufp_event_obj="last_motion_event",
|
||||
),
|
||||
@ -350,7 +350,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
key="smart_obj_any",
|
||||
name="Object Detected",
|
||||
icon="mdi:eye",
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_value="is_smart_currently_detected",
|
||||
ufp_required_field="feature_flags.has_smart_detect",
|
||||
ufp_event_obj="last_smart_detect_event",
|
||||
),
|
||||
@ -358,7 +358,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
key="smart_obj_person",
|
||||
name="Person Detected",
|
||||
icon="mdi:walk",
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_value="is_person_currently_detected",
|
||||
ufp_required_field="can_detect_person",
|
||||
ufp_enabled="is_person_detection_on",
|
||||
ufp_event_obj="last_person_detect_event",
|
||||
@ -367,25 +367,16 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
key="smart_obj_vehicle",
|
||||
name="Vehicle Detected",
|
||||
icon="mdi:car",
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_value="is_vehicle_currently_detected",
|
||||
ufp_required_field="can_detect_vehicle",
|
||||
ufp_enabled="is_vehicle_detection_on",
|
||||
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(
|
||||
key="smart_obj_package",
|
||||
name="Package Detected",
|
||||
icon="mdi:package-variant-closed",
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_value="is_package_currently_detected",
|
||||
ufp_required_field="can_detect_package",
|
||||
ufp_enabled="is_package_detection_on",
|
||||
ufp_event_obj="last_package_detect_event",
|
||||
@ -394,7 +385,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
key="smart_audio_any",
|
||||
name="Audio Object Detected",
|
||||
icon="mdi:eye",
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_value="is_audio_currently_detected",
|
||||
ufp_required_field="feature_flags.has_smart_detect",
|
||||
ufp_event_obj="last_smart_audio_detect_event",
|
||||
),
|
||||
@ -402,7 +393,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
key="smart_audio_smoke",
|
||||
name="Smoke Alarm Detected",
|
||||
icon="mdi:fire",
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_value="is_smoke_currently_detected",
|
||||
ufp_required_field="can_detect_smoke",
|
||||
ufp_enabled="is_smoke_detection_on",
|
||||
ufp_event_obj="last_smoke_detect_event",
|
||||
@ -410,10 +401,10 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
ProtectBinaryEventEntityDescription(
|
||||
key="smart_audio_cmonx",
|
||||
name="CO Alarm Detected",
|
||||
icon="mdi:fire",
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_required_field="can_detect_smoke",
|
||||
ufp_enabled="is_smoke_detection_on",
|
||||
icon="mdi:molecule-co",
|
||||
ufp_value="is_cmonx_currently_detected",
|
||||
ufp_required_field="can_detect_co",
|
||||
ufp_enabled="is_co_detection_on",
|
||||
ufp_event_obj="last_cmonx_detect_event",
|
||||
),
|
||||
)
|
||||
@ -619,7 +610,7 @@ class ProtectEventBinarySensor(EventEntityMixin, BinarySensorEntity):
|
||||
@callback
|
||||
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
|
||||
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
|
||||
if not is_on:
|
||||
self._event = None
|
||||
|
@ -41,7 +41,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyunifiprotect", "unifi_discovery"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyunifiprotect==4.22.5", "unifi-discovery==1.1.7"],
|
||||
"requirements": ["pyunifiprotect==4.23.2", "unifi-discovery==1.1.7"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
|
||||
from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel
|
||||
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
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 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."""
|
||||
if event is None:
|
||||
return False
|
||||
|
||||
now = dt_util.utcnow()
|
||||
value = now > event.start
|
||||
if value and event.end is not None and now > event.end:
|
||||
value = False
|
||||
|
||||
return value
|
||||
return event is not None and self.get_ufp_value(obj)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
@ -527,7 +527,7 @@ EVENT_SENSORS: tuple[ProtectSensorEventEntityDescription, ...] = (
|
||||
name="License Plate Detected",
|
||||
icon="mdi:car",
|
||||
translation_key="license_plate",
|
||||
ufp_value="is_smart_detected",
|
||||
ufp_value="is_license_plate_currently_detected",
|
||||
ufp_required_field="can_detect_license_plate",
|
||||
ufp_event_obj="last_license_plate_detect_event",
|
||||
),
|
||||
@ -781,7 +781,7 @@ class ProtectEventSensor(EventEntityMixin, SensorEntity):
|
||||
EventEntityMixin._async_update_device_from_protect(self, device)
|
||||
event = self._event
|
||||
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 = (
|
||||
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_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(
|
||||
key="motion",
|
||||
name="Detections: Motion",
|
||||
@ -167,17 +177,6 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
||||
ufp_set_method="set_vehicle_detection",
|
||||
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(
|
||||
key="smart_package",
|
||||
name="Detections: Package",
|
||||
@ -202,7 +201,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
||||
),
|
||||
ProtectSwitchEntityDescription(
|
||||
key="smart_smoke",
|
||||
name="Detections: Smoke/CO",
|
||||
name="Detections: Smoke",
|
||||
icon="mdi:fire",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_required_field="can_detect_smoke",
|
||||
@ -212,13 +211,14 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
||||
ufp_perm=PermRequired.WRITE,
|
||||
),
|
||||
ProtectSwitchEntityDescription(
|
||||
key="color_night_vision",
|
||||
name="Color Night Vision",
|
||||
icon="mdi:light-flood-down",
|
||||
key="smart_cmonx",
|
||||
name="Detections: CO",
|
||||
icon="mdi:molecule-co",
|
||||
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_required_field="can_detect_co",
|
||||
ufp_value="is_co_detection_on",
|
||||
ufp_enabled="is_recording_enabled",
|
||||
ufp_set_method="set_cmonx_detection",
|
||||
ufp_perm=PermRequired.WRITE,
|
||||
),
|
||||
)
|
||||
|
@ -186,9 +186,10 @@ class ValveEntity(Entity):
|
||||
|
||||
@final
|
||||
@property
|
||||
def state_attributes(self) -> dict[str, Any]:
|
||||
def state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return the state attributes."""
|
||||
|
||||
if not self.reports_position:
|
||||
return None
|
||||
return {ATTR_CURRENT_POSITION: self.current_valve_position}
|
||||
|
||||
@property
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Support for ZoneMinder."""
|
||||
import logging
|
||||
|
||||
from requests.exceptions import ConnectionError as RequestsConnectionError
|
||||
import voluptuous as vol
|
||||
from zoneminder.zm import ZoneMinder
|
||||
|
||||
@ -75,7 +76,14 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
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:
|
||||
"""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.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
@ -28,8 +29,9 @@ def setup_platform(
|
||||
zm_client: ZoneMinder
|
||||
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
||||
if not (monitors := zm_client.get_monitors()):
|
||||
_LOGGER.warning("Could not fetch monitors from ZoneMinder host: %s")
|
||||
return
|
||||
raise PlatformNotReady(
|
||||
"Camera could not fetch any monitors from ZoneMinder"
|
||||
)
|
||||
|
||||
for monitor in monitors:
|
||||
_LOGGER.info("Initializing camera %s", monitor.id)
|
||||
|
@ -1,9 +1,9 @@
|
||||
{
|
||||
"domain": "zoneminder",
|
||||
"name": "ZoneMinder",
|
||||
"codeowners": ["@rohankapoorcom"],
|
||||
"codeowners": ["@rohankapoorcom", "@nabbi"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/zoneminder",
|
||||
"iot_class": "local_polling",
|
||||
"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.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@ -77,7 +78,9 @@ def setup_platform(
|
||||
zm_client: ZoneMinder
|
||||
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
||||
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:
|
||||
sensors.append(ZMSensorMonitors(monitor))
|
||||
|
@ -11,6 +11,7 @@ from zoneminder.zm import ZoneMinder
|
||||
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
|
||||
from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@ -42,8 +43,9 @@ def setup_platform(
|
||||
zm_client: ZoneMinder
|
||||
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
|
||||
if not (monitors := zm_client.get_monitors()):
|
||||
_LOGGER.warning("Could not fetch monitors from ZoneMinder")
|
||||
return
|
||||
raise PlatformNotReady(
|
||||
"Switch could not fetch any monitors from ZoneMinder"
|
||||
)
|
||||
|
||||
for monitor in monitors:
|
||||
switches.append(ZMSwitchMonitors(monitor, on_state, off_state))
|
||||
|
@ -16,7 +16,7 @@ from .helpers.deprecation import (
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 1
|
||||
PATCH_VERSION: Final = "2"
|
||||
PATCH_VERSION: Final = "3"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
||||
|
@ -13,6 +13,7 @@ import logging
|
||||
import math
|
||||
import sys
|
||||
from timeit import default_timer as timer
|
||||
from types import FunctionType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@ -374,6 +375,9 @@ class CachedProperties(type):
|
||||
# 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.
|
||||
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))
|
||||
annotations = cls.__annotations__
|
||||
if attr_name in annotations:
|
||||
|
@ -14,7 +14,7 @@ bcrypt==4.0.1
|
||||
bleak-retry-connector==3.4.0
|
||||
bleak==0.21.1
|
||||
bluetooth-adapters==0.16.2
|
||||
bluetooth-auto-recovery==1.2.3
|
||||
bluetooth-auto-recovery==1.3.0
|
||||
bluetooth-data-tools==1.19.0
|
||||
cached_ipaddress==0.3.0
|
||||
certifi>=2021.5.30
|
||||
@ -24,10 +24,10 @@ dbus-fast==2.21.0
|
||||
fnv-hash-fast==0.5.0
|
||||
ha-av==10.1.1
|
||||
ha-ffmpeg==3.1.0
|
||||
habluetooth==2.0.2
|
||||
habluetooth==2.1.0
|
||||
hass-nabucasa==0.75.1
|
||||
hassil==1.5.1
|
||||
home-assistant-bluetooth==1.11.0
|
||||
home-assistant-bluetooth==1.12.0
|
||||
home-assistant-frontend==20240104.0
|
||||
home-assistant-intents==2024.1.2
|
||||
httpx==0.26.0
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.1.2"
|
||||
version = "2024.1.3"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
@ -37,7 +37,7 @@ dependencies = [
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.26.0",
|
||||
"home-assistant-bluetooth==1.11.0",
|
||||
"home-assistant-bluetooth==1.12.0",
|
||||
"ifaddr==0.2.0",
|
||||
"Jinja2==3.1.2",
|
||||
"lru-dict==1.3.0",
|
||||
|
@ -15,7 +15,7 @@ bcrypt==4.0.1
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.3.0
|
||||
httpx==0.26.0
|
||||
home-assistant-bluetooth==1.11.0
|
||||
home-assistant-bluetooth==1.12.0
|
||||
ifaddr==0.2.0
|
||||
Jinja2==3.1.2
|
||||
lru-dict==1.3.0
|
||||
|
@ -167,7 +167,7 @@ afsapi==0.2.7
|
||||
agent-py==0.0.23
|
||||
|
||||
# homeassistant.components.geo_json_events
|
||||
aio-geojson-generic-client==0.3
|
||||
aio-geojson-generic-client==0.4
|
||||
|
||||
# homeassistant.components.geonetnz_quakes
|
||||
aio-geojson-geonetnz-quakes==0.15
|
||||
@ -215,7 +215,7 @@ aiobafi6==0.9.0
|
||||
aiobotocore==2.6.0
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==0.7.0
|
||||
aiocomelit==0.7.3
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==1.6.0
|
||||
@ -356,7 +356,7 @@ aioruuvigateway==0.1.0
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==7.0.0
|
||||
aioshelly==7.1.0
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@ -368,7 +368,7 @@ aioslimproto==2.3.3
|
||||
aiosteamist==0.3.2
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==3.3.0
|
||||
aioswitcher==3.4.1
|
||||
|
||||
# homeassistant.components.syncthing
|
||||
aiosyncthing==0.5.1
|
||||
@ -547,7 +547,7 @@ bleak==0.21.1
|
||||
blebox-uniapi==2.2.0
|
||||
|
||||
# homeassistant.components.blink
|
||||
blinkpy==0.22.4
|
||||
blinkpy==0.22.5
|
||||
|
||||
# homeassistant.components.bitcoin
|
||||
blockchain==1.4.4
|
||||
@ -566,7 +566,7 @@ bluemaestro-ble==0.2.3
|
||||
bluetooth-adapters==0.16.2
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==1.2.3
|
||||
bluetooth-auto-recovery==1.3.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
# homeassistant.components.ld2410_ble
|
||||
@ -998,7 +998,7 @@ ha-philipsjs==3.1.1
|
||||
habitipy==0.2.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==2.0.2
|
||||
habluetooth==2.1.0
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.75.1
|
||||
@ -1240,7 +1240,7 @@ maxcube-api==0.4.3
|
||||
mbddns==0.1.2
|
||||
|
||||
# homeassistant.components.minecraft_server
|
||||
mcstatus==11.0.0
|
||||
mcstatus==11.1.1
|
||||
|
||||
# homeassistant.components.meater
|
||||
meater-python==0.0.8
|
||||
@ -1548,7 +1548,7 @@ pushover_complete==1.1.1
|
||||
pvo==2.1.1
|
||||
|
||||
# homeassistant.components.aosmith
|
||||
py-aosmith==1.0.1
|
||||
py-aosmith==1.0.4
|
||||
|
||||
# homeassistant.components.canary
|
||||
py-canary==0.5.3
|
||||
@ -2280,7 +2280,7 @@ pytrydan==0.4.0
|
||||
pyudev==0.23.2
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
pyunifiprotect==4.22.5
|
||||
pyunifiprotect==4.23.2
|
||||
|
||||
# homeassistant.components.uptimerobot
|
||||
pyuptimerobot==22.2.0
|
||||
@ -2376,7 +2376,7 @@ renault-api==0.2.1
|
||||
renson-endura-delta==1.7.1
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.8.5
|
||||
reolink-aio==0.8.6
|
||||
|
||||
# homeassistant.components.idteck_prox
|
||||
rfk101py==0.0.1
|
||||
@ -2890,7 +2890,7 @@ zigpy-znp==0.12.1
|
||||
zigpy==0.60.4
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.2
|
||||
zm-py==0.5.4
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.55.3
|
||||
|
@ -146,7 +146,7 @@ afsapi==0.2.7
|
||||
agent-py==0.0.23
|
||||
|
||||
# homeassistant.components.geo_json_events
|
||||
aio-geojson-generic-client==0.3
|
||||
aio-geojson-generic-client==0.4
|
||||
|
||||
# homeassistant.components.geonetnz_quakes
|
||||
aio-geojson-geonetnz-quakes==0.15
|
||||
@ -194,7 +194,7 @@ aiobafi6==0.9.0
|
||||
aiobotocore==2.6.0
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==0.7.0
|
||||
aiocomelit==0.7.3
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==1.6.0
|
||||
@ -329,7 +329,7 @@ aioruuvigateway==0.1.0
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==7.0.0
|
||||
aioshelly==7.1.0
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@ -341,7 +341,7 @@ aioslimproto==2.3.3
|
||||
aiosteamist==0.3.2
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==3.3.0
|
||||
aioswitcher==3.4.1
|
||||
|
||||
# homeassistant.components.syncthing
|
||||
aiosyncthing==0.5.1
|
||||
@ -466,7 +466,7 @@ bleak==0.21.1
|
||||
blebox-uniapi==2.2.0
|
||||
|
||||
# homeassistant.components.blink
|
||||
blinkpy==0.22.4
|
||||
blinkpy==0.22.5
|
||||
|
||||
# homeassistant.components.blue_current
|
||||
bluecurrent-api==1.0.6
|
||||
@ -478,7 +478,7 @@ bluemaestro-ble==0.2.3
|
||||
bluetooth-adapters==0.16.2
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==1.2.3
|
||||
bluetooth-auto-recovery==1.3.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
# homeassistant.components.ld2410_ble
|
||||
@ -803,7 +803,7 @@ ha-philipsjs==3.1.1
|
||||
habitipy==0.2.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==2.0.2
|
||||
habluetooth==2.1.0
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.75.1
|
||||
@ -976,7 +976,7 @@ maxcube-api==0.4.3
|
||||
mbddns==0.1.2
|
||||
|
||||
# homeassistant.components.minecraft_server
|
||||
mcstatus==11.0.0
|
||||
mcstatus==11.1.1
|
||||
|
||||
# homeassistant.components.meater
|
||||
meater-python==0.0.8
|
||||
@ -1195,7 +1195,7 @@ pushover_complete==1.1.1
|
||||
pvo==2.1.1
|
||||
|
||||
# homeassistant.components.aosmith
|
||||
py-aosmith==1.0.1
|
||||
py-aosmith==1.0.4
|
||||
|
||||
# homeassistant.components.canary
|
||||
py-canary==0.5.3
|
||||
@ -1726,7 +1726,7 @@ pytrydan==0.4.0
|
||||
pyudev==0.23.2
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
pyunifiprotect==4.22.5
|
||||
pyunifiprotect==4.23.2
|
||||
|
||||
# homeassistant.components.uptimerobot
|
||||
pyuptimerobot==22.2.0
|
||||
@ -1798,7 +1798,7 @@ renault-api==0.2.1
|
||||
renson-endura-delta==1.7.1
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.8.5
|
||||
reolink-aio==0.8.6
|
||||
|
||||
# homeassistant.components.rflink
|
||||
rflink==0.0.65
|
||||
|
@ -224,9 +224,20 @@ class ReportedProperties:
|
||||
|
||||
def assert_equal(self, namespace, name, value):
|
||||
"""Assert a property is equal to a given value."""
|
||||
prop_set = None
|
||||
prop_count = 0
|
||||
for prop in self.properties:
|
||||
if prop["namespace"] == namespace and prop["name"] == name:
|
||||
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}")
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Test Smart Home HTTP endpoints."""
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
@ -44,11 +45,16 @@ async def do_http_discovery(config, hass, hass_client):
|
||||
],
|
||||
)
|
||||
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:
|
||||
"""With `smart_home:` HTTP API is exposed."""
|
||||
response = await do_http_discovery(config, hass, hass_client)
|
||||
response_data = await response.json()
|
||||
"""With `smart_home:` HTTP API is exposed and debug log is redacted."""
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
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
|
||||
# covered in other tests.
|
||||
@ -61,5 +67,4 @@ async def test_http_api_disabled(
|
||||
"""Without `smart_home:`, the HTTP API is disabled."""
|
||||
config = {"alexa": {}}
|
||||
response = await do_http_discovery(config, hass, hass_client)
|
||||
|
||||
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_data.json", DOMAIN
|
||||
)
|
||||
get_all_device_info_fixture = load_json_object_fixture(
|
||||
"get_all_device_info.json", DOMAIN
|
||||
)
|
||||
|
||||
client_mock = MagicMock(AOSmithAPIClient)
|
||||
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_all_device_info = AsyncMock(
|
||||
return_value=get_all_device_info_fixture
|
||||
)
|
||||
|
||||
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(
|
||||
"bluetooth_adapters.systems.platform.system",
|
||||
return_value="Darwin",
|
||||
):
|
||||
), patch("habluetooth.scanner.SYSTEM", "Darwin"):
|
||||
yield
|
||||
|
||||
|
||||
@ -65,7 +65,7 @@ def windows_adapter():
|
||||
with patch(
|
||||
"bluetooth_adapters.systems.platform.system",
|
||||
return_value="Windows",
|
||||
):
|
||||
), patch("habluetooth.scanner.SYSTEM", "Windows"):
|
||||
yield
|
||||
|
||||
|
||||
@ -81,7 +81,7 @@ def no_adapter_fixture():
|
||||
), patch(
|
||||
"bluetooth_adapters.systems.platform.system",
|
||||
return_value="Linux",
|
||||
), patch(
|
||||
), patch("habluetooth.scanner.SYSTEM", "Linux"), patch(
|
||||
"bluetooth_adapters.systems.linux.LinuxAdapters.refresh",
|
||||
), patch(
|
||||
"bluetooth_adapters.systems.linux.LinuxAdapters.adapters",
|
||||
@ -102,7 +102,7 @@ def one_adapter_fixture():
|
||||
), patch(
|
||||
"bluetooth_adapters.systems.platform.system",
|
||||
return_value="Linux",
|
||||
), patch(
|
||||
), patch("habluetooth.scanner.SYSTEM", "Linux"), patch(
|
||||
"bluetooth_adapters.systems.linux.LinuxAdapters.refresh",
|
||||
), patch(
|
||||
"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
|
||||
|
||||
|
||||
@pytest.mark.skipif("platform.system() != 'Darwin'")
|
||||
async def test_setup_and_stop_macos(
|
||||
hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None
|
||||
) -> None:
|
||||
|
@ -76,16 +76,9 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock, None]:
|
||||
|
||||
# Attributes that we mock with default values.
|
||||
|
||||
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"
|
||||
mock_cloud.id_token = None
|
||||
mock_cloud.access_token = None
|
||||
mock_cloud.refresh_token = None
|
||||
|
||||
# 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.
|
||||
"""
|
||||
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]
|
||||
await on_start_callback()
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
@ -113,8 +113,8 @@ async def setup_cloud_fixture(hass: HomeAssistant, cloud: MagicMock) -> None:
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
on_start_callback = cloud.register_on_start.call_args[0][0]
|
||||
await on_start_callback()
|
||||
await cloud.login("test-user", "test-pass")
|
||||
cloud.login.reset_mock()
|
||||
|
||||
|
||||
async def test_google_actions_sync(
|
||||
|
@ -19,7 +19,7 @@ from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.exceptions import Unauthorized
|
||||
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:
|
||||
@ -230,6 +230,7 @@ async def test_async_get_or_create_cloudhook(
|
||||
"""Test async_get_or_create_cloudhook."""
|
||||
assert await async_setup_component(hass, "cloud", {"cloud": {}})
|
||||
await hass.async_block_till_done()
|
||||
await cloud.login("test-user", "test-pass")
|
||||
|
||||
webhook_id = "mock-webhook-id"
|
||||
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()
|
||||
|
||||
# Simulate logged out
|
||||
cloud.id_token = None
|
||||
await cloud.logout()
|
||||
|
||||
# Not logged in
|
||||
with pytest.raises(CloudNotAvailable):
|
||||
@ -274,3 +275,18 @@ async def test_async_get_or_create_cloudhook(
|
||||
# Not connected
|
||||
with pytest.raises(CloudNotConnected):
|
||||
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 cloud.login("test-user", "test-pass")
|
||||
|
||||
cloud.remote.snitun_server = "us-west-1"
|
||||
cloud.remote.certificate_status = CertificateStatus.READY
|
||||
|
@ -4,7 +4,7 @@ from http import HTTPStatus
|
||||
from typing import Any
|
||||
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 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["gender"] == "female"
|
||||
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
|
||||
# ent4 = cover with all tilt functions but no position
|
||||
# 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
|
||||
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, ent4)
|
||||
assert is_open(hass, ent5)
|
||||
assert is_open(hass, ent6)
|
||||
|
||||
# call basic toggle services
|
||||
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, ent4)
|
||||
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_closing(hass, ent2)
|
||||
assert is_closed(hass, ent3)
|
||||
assert is_closed(hass, ent4)
|
||||
assert is_closing(hass, ent5)
|
||||
assert is_closing(hass, ent6)
|
||||
|
||||
# call basic toggle services and set different cover position states
|
||||
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)
|
||||
set_cover_position(ent5, 15)
|
||||
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
|
||||
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, ent4)
|
||||
assert is_open(hass, ent5)
|
||||
assert is_opening(hass, ent6)
|
||||
|
||||
# call basic toggle services
|
||||
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, ent4)
|
||||
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
|
||||
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, ent4)
|
||||
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):
|
||||
@ -100,6 +113,11 @@ def set_cover_position(ent, position) -> None:
|
||||
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):
|
||||
"""Return if the cover is closed based on the statemachine."""
|
||||
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.state == "off"
|
||||
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))
|
||||
assert len(calls) == 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}"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(hass.data[DOMAIN].telegrams._jobs) == 0
|
||||
await knx.receive_write("0/0/1", (0x03, 0x2F))
|
||||
assert len(calls) == 0
|
||||
|
||||
|
@ -41,6 +41,7 @@ TEST_JAVA_STATUS_RESPONSE = JavaStatusResponse(
|
||||
version=JavaStatusVersion.build(TEST_JAVA_STATUS_RESPONSE_RAW["version"]),
|
||||
motd=Motd.parse(TEST_JAVA_STATUS_RESPONSE_RAW["description"], bedrock=False),
|
||||
icon=None,
|
||||
enforces_secure_chat=False,
|
||||
latency=5,
|
||||
)
|
||||
|
||||
|
@ -115,6 +115,63 @@ async def test_controlling_state_via_topic(
|
||||
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(
|
||||
"hass_config",
|
||||
[
|
||||
@ -211,7 +268,7 @@ async def test_attribute_validation_max_greater_then_min(
|
||||
) -> None:
|
||||
"""Test the validation of min and max configuration attributes."""
|
||||
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(
|
||||
|
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_ID3 = "bada77"
|
||||
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_NAME2 = "Heater FE12"
|
||||
DUMMY_DEVICE_NAME3 = "Breeze AB39"
|
||||
@ -67,6 +71,7 @@ DUMMY_PLUG_DEVICE = SwitcherPowerPlug(
|
||||
DeviceType.POWER_PLUG,
|
||||
DeviceState.ON,
|
||||
DUMMY_DEVICE_ID1,
|
||||
DUMMY_DEVICE_KEY1,
|
||||
DUMMY_IP_ADDRESS1,
|
||||
DUMMY_MAC_ADDRESS1,
|
||||
DUMMY_DEVICE_NAME1,
|
||||
@ -78,6 +83,7 @@ DUMMY_WATER_HEATER_DEVICE = SwitcherWaterHeater(
|
||||
DeviceType.V4,
|
||||
DeviceState.ON,
|
||||
DUMMY_DEVICE_ID2,
|
||||
DUMMY_DEVICE_KEY2,
|
||||
DUMMY_IP_ADDRESS2,
|
||||
DUMMY_MAC_ADDRESS2,
|
||||
DUMMY_DEVICE_NAME2,
|
||||
@ -91,6 +97,7 @@ DUMMY_SHUTTER_DEVICE = SwitcherShutter(
|
||||
DeviceType.RUNNER,
|
||||
DeviceState.ON,
|
||||
DUMMY_DEVICE_ID4,
|
||||
DUMMY_DEVICE_KEY4,
|
||||
DUMMY_IP_ADDRESS4,
|
||||
DUMMY_MAC_ADDRESS4,
|
||||
DUMMY_DEVICE_NAME4,
|
||||
@ -102,6 +109,7 @@ DUMMY_THERMOSTAT_DEVICE = SwitcherThermostat(
|
||||
DeviceType.BREEZE,
|
||||
DeviceState.ON,
|
||||
DUMMY_DEVICE_ID3,
|
||||
DUMMY_DEVICE_KEY3,
|
||||
DUMMY_IP_ADDRESS3,
|
||||
DUMMY_MAC_ADDRESS3,
|
||||
DUMMY_DEVICE_NAME3,
|
||||
|
@ -25,6 +25,7 @@ async def test_diagnostics(
|
||||
{
|
||||
"auto_shutdown": "02:00:00",
|
||||
"device_id": REDACTED,
|
||||
"device_key": REDACTED,
|
||||
"device_state": {
|
||||
"__type": "<enum 'DeviceState'>",
|
||||
"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"
|
||||
devices_fixture = "tado/devices.json"
|
||||
mobile_devices_fixture = "tado/mobile_devices.json"
|
||||
me_fixture = "tado/me.json"
|
||||
weather_fixture = "tado/weather.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",
|
||||
text=load_fixture(devices_fixture),
|
||||
)
|
||||
m.get(
|
||||
"https://my.tado.com/api/v2/homes/1/mobileDevices",
|
||||
text=load_fixture(mobile_devices_fixture),
|
||||
)
|
||||
m.get(
|
||||
"https://my.tado.com/api/v2/devices/WR1/",
|
||||
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.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:
|
||||
"""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