This commit is contained in:
Franck Nijhof 2024-01-12 18:01:20 +01:00 committed by GitHub
commit 99ee57aefc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
105 changed files with 1612 additions and 263 deletions

View File

@ -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

View File

@ -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"]

View File

@ -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:

View File

@ -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.

View 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)

View File

@ -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)

View File

@ -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)

View File

@ -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:

View 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)

View File

@ -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"]
}

View File

@ -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]]):

View File

@ -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"]
}

View File

@ -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"
]
}

View File

@ -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),

View File

@ -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(

View File

@ -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."""

View File

@ -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"]
}

View File

@ -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"]

View File

@ -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])

View File

@ -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

View File

@ -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)},

View File

@ -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."""

View File

@ -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,

View File

@ -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"]
}

View File

@ -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(
[

View File

@ -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}"

View File

@ -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]

View File

@ -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,
)

View File

@ -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)

View File

@ -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)

View File

@ -3,7 +3,7 @@
"step": {
"user": {
"data": {
"station_id": "Sensor ID",
"sensor_id": "Sensor ID",
"show_on_map": "Show on map"
}
}

View File

@ -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):

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["dnspython", "mcstatus"],
"quality_scale": "gold",
"requirements": ["mcstatus==11.0.0"]
"requirements": ["mcstatus==11.1.1"]
}

View File

@ -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}")

View File

@ -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"]
}

View File

@ -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()

View File

@ -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.",

View File

@ -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),
)

View File

@ -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:

View File

@ -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

View File

@ -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,

View File

@ -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:

View File

@ -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,

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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(

View File

@ -7,5 +7,5 @@
"iot_class": "local_push",
"loggers": ["aioswitcher"],
"quality_scale": "platinum",
"requirements": ["aioswitcher==3.3.0"]
"requirements": ["aioswitcher==3.4.1"]
}

View File

@ -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:

View File

@ -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 :]

View File

@ -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))

View File

@ -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)

View File

@ -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"

View File

@ -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,
)
)

View File

@ -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()

View File

@ -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)

View File

@ -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, []):

View File

@ -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,
),

View File

@ -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

View File

@ -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",

View File

@ -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)

View File

@ -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"
)

View File

@ -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,
),
)

View File

@ -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

View File

@ -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."""

View File

@ -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)

View File

@ -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"]
}

View File

@ -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))

View File

@ -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))

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}")

View File

@ -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

View File

@ -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

View 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"
}
}
}

View 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',
}),
}),
})
# ---

View 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
)

View File

@ -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",

View File

@ -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:

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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",
}
]
}
}

View File

@ -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

View File

@ -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,
)

View File

@ -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(

View 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")
)
)

View File

@ -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,

View File

@ -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')>",

View 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"
}
}
]

View File

@ -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),

View File

@ -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