This commit is contained in:
Franck Nijhof 2024-01-30 19:38:38 +01:00 committed by GitHub
commit 1f7bf7c2a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 170 additions and 86 deletions

View File

@ -40,10 +40,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
f"Could not find Airthings device with address {address}" f"Could not find Airthings device with address {address}"
) )
airthings = AirthingsBluetoothDeviceData(_LOGGER, elevation, is_metric)
async def _async_update_method() -> AirthingsDevice: async def _async_update_method() -> AirthingsDevice:
"""Get data from Airthings BLE.""" """Get data from Airthings BLE."""
ble_device = bluetooth.async_ble_device_from_address(hass, address) ble_device = bluetooth.async_ble_device_from_address(hass, address)
airthings = AirthingsBluetoothDeviceData(_LOGGER, elevation, is_metric)
try: try:
data = await airthings.update_device(ble_device) # type: ignore[arg-type] data = await airthings.update_device(ble_device) # type: ignore[arg-type]

View File

@ -24,5 +24,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble", "documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["airthings-ble==0.5.6-2"] "requirements": ["airthings-ble==0.6.0"]
} }

View File

@ -860,8 +860,8 @@ class AlexaInputController(AlexaCapability):
def inputs(self) -> list[dict[str, str]] | None: def inputs(self) -> list[dict[str, str]] | None:
"""Return the list of valid supported inputs.""" """Return the list of valid supported inputs."""
source_list: list[Any] = self.entity.attributes.get( source_list: list[Any] = (
media_player.ATTR_INPUT_SOURCE_LIST, [] self.entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST) or []
) )
return AlexaInputController.get_valid_inputs(source_list) return AlexaInputController.get_valid_inputs(source_list)
@ -1196,7 +1196,7 @@ class AlexaThermostatController(AlexaCapability):
return None return None
supported_modes: list[str] = [] supported_modes: list[str] = []
hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES, []) hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES) or []
for mode in hvac_modes: for mode in hvac_modes:
if thermostat_mode := API_THERMOSTAT_MODES.get(mode): if thermostat_mode := API_THERMOSTAT_MODES.get(mode):
supported_modes.append(thermostat_mode) supported_modes.append(thermostat_mode)
@ -1422,18 +1422,22 @@ class AlexaModeController(AlexaCapability):
# Humidifier mode # Humidifier mode
if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}": if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}":
mode = self.entity.attributes.get(humidifier.ATTR_MODE, None) mode = self.entity.attributes.get(humidifier.ATTR_MODE)
if mode in self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []): modes: list[str] = (
self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES) or []
)
if mode in modes:
return f"{humidifier.ATTR_MODE}.{mode}" return f"{humidifier.ATTR_MODE}.{mode}"
# Water heater operation mode # Water heater operation mode
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}": if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
operation_mode = self.entity.attributes.get( operation_mode = self.entity.attributes.get(
water_heater.ATTR_OPERATION_MODE, None water_heater.ATTR_OPERATION_MODE
) )
if operation_mode in self.entity.attributes.get( operation_modes: list[str] = (
water_heater.ATTR_OPERATION_LIST, [] self.entity.attributes.get(water_heater.ATTR_OPERATION_LIST) or []
): )
if operation_mode in operation_modes:
return f"{water_heater.ATTR_OPERATION_MODE}.{operation_mode}" return f"{water_heater.ATTR_OPERATION_MODE}.{operation_mode}"
# Cover Position # Cover Position
@ -1492,7 +1496,7 @@ class AlexaModeController(AlexaCapability):
self._resource = AlexaModeResource( self._resource = AlexaModeResource(
[AlexaGlobalCatalog.SETTING_PRESET], False [AlexaGlobalCatalog.SETTING_PRESET], False
) )
preset_modes = self.entity.attributes.get(fan.ATTR_PRESET_MODES, []) preset_modes = self.entity.attributes.get(fan.ATTR_PRESET_MODES) or []
for preset_mode in preset_modes: for preset_mode in preset_modes:
self._resource.add_mode( self._resource.add_mode(
f"{fan.ATTR_PRESET_MODE}.{preset_mode}", [preset_mode] f"{fan.ATTR_PRESET_MODE}.{preset_mode}", [preset_mode]
@ -1508,7 +1512,7 @@ class AlexaModeController(AlexaCapability):
# Humidifier modes # Humidifier modes
if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}": if self.instance == f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}":
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False) self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
modes = self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []) modes = self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES) or []
for mode in modes: for mode in modes:
self._resource.add_mode(f"{humidifier.ATTR_MODE}.{mode}", [mode]) self._resource.add_mode(f"{humidifier.ATTR_MODE}.{mode}", [mode])
# Humidifiers or Fans with a single mode completely break Alexa discovery, # Humidifiers or Fans with a single mode completely break Alexa discovery,
@ -1522,8 +1526,8 @@ class AlexaModeController(AlexaCapability):
# Water heater operation modes # Water heater operation modes
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}": if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False) self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
operation_modes = self.entity.attributes.get( operation_modes = (
water_heater.ATTR_OPERATION_LIST, [] self.entity.attributes.get(water_heater.ATTR_OPERATION_LIST) or []
) )
for operation_mode in operation_modes: for operation_mode in operation_modes:
self._resource.add_mode( self._resource.add_mode(
@ -2368,7 +2372,7 @@ class AlexaEqualizerController(AlexaCapability):
"""Return the sound modes supported in the configurations object.""" """Return the sound modes supported in the configurations object."""
configurations = None configurations = None
supported_sound_modes = self.get_valid_inputs( supported_sound_modes = self.get_valid_inputs(
self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST, []) self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) or []
) )
if supported_sound_modes: if supported_sound_modes:
configurations = {"modes": {"supported": supported_sound_modes}} configurations = {"modes": {"supported": supported_sound_modes}}

View File

@ -478,7 +478,7 @@ class ClimateCapabilities(AlexaEntity):
if ( if (
self.entity.domain == climate.DOMAIN self.entity.domain == climate.DOMAIN
and climate.HVACMode.OFF and climate.HVACMode.OFF
in self.entity.attributes.get(climate.ATTR_HVAC_MODES, []) in (self.entity.attributes.get(climate.ATTR_HVAC_MODES) or [])
or self.entity.domain == water_heater.DOMAIN or self.entity.domain == water_heater.DOMAIN
and (supported_features & water_heater.WaterHeaterEntityFeature.ON_OFF) and (supported_features & water_heater.WaterHeaterEntityFeature.ON_OFF)
): ):
@ -742,7 +742,8 @@ class MediaPlayerCapabilities(AlexaEntity):
and domain != "denonavr" and domain != "denonavr"
): ):
inputs = AlexaEqualizerController.get_valid_inputs( inputs = AlexaEqualizerController.get_valid_inputs(
self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST, []) self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
or []
) )
if len(inputs) > 0: if len(inputs) > 0:
yield AlexaEqualizerController(self.entity) yield AlexaEqualizerController(self.entity)

View File

@ -570,7 +570,7 @@ async def async_api_select_input(
# Attempt to map the ALL UPPERCASE payload name to a source. # Attempt to map the ALL UPPERCASE payload name to a source.
# Strips trailing 1 to match single input devices. # Strips trailing 1 to match single input devices.
source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST, []) source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST) or []
for source in source_list: for source in source_list:
formatted_source = ( formatted_source = (
source.lower().replace("-", "").replace("_", "").replace(" ", "") source.lower().replace("-", "").replace("_", "").replace(" ", "")
@ -987,7 +987,7 @@ async def async_api_set_thermostat_mode(
ha_preset = next((k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode), None) ha_preset = next((k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode), None)
if ha_preset: if ha_preset:
presets = entity.attributes.get(climate.ATTR_PRESET_MODES, []) presets = entity.attributes.get(climate.ATTR_PRESET_MODES) or []
if ha_preset not in presets: if ha_preset not in presets:
msg = f"The requested thermostat mode {ha_preset} is not supported" msg = f"The requested thermostat mode {ha_preset} is not supported"
@ -997,7 +997,7 @@ async def async_api_set_thermostat_mode(
data[climate.ATTR_PRESET_MODE] = ha_preset data[climate.ATTR_PRESET_MODE] = ha_preset
elif mode == "CUSTOM": elif mode == "CUSTOM":
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES, []) operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) or []
custom_mode = directive.payload["thermostatMode"]["customName"] custom_mode = directive.payload["thermostatMode"]["customName"]
custom_mode = next( custom_mode = next(
(k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode), (k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode),
@ -1013,7 +1013,7 @@ async def async_api_set_thermostat_mode(
data[climate.ATTR_HVAC_MODE] = custom_mode data[climate.ATTR_HVAC_MODE] = custom_mode
else: else:
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES, []) operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) or []
ha_modes: dict[str, str] = { ha_modes: dict[str, str] = {
k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode
} }

View File

@ -52,9 +52,8 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
# Test the connection to the host and get the current status for serial number. # Test the connection to the host and get the current status for serial number.
coordinator = APCUPSdCoordinator(self.hass, host, port) coordinator = APCUPSdCoordinator(self.hass, host, port)
await coordinator.async_request_refresh() await coordinator.async_request_refresh()
await self.hass.async_block_till_done()
if isinstance(coordinator.last_exception, (UpdateFailed, asyncio.TimeoutError)): if isinstance(coordinator.last_exception, (UpdateFailed, asyncio.TimeoutError)):
errors = {"base": "cannot_connect"} errors = {"base": "cannot_connect"}
return self.async_show_form( return self.async_show_form(

View File

@ -386,7 +386,7 @@ class HueOneLightChangeView(HomeAssistantView):
# Get the entity's supported features # Get the entity's supported features
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if entity.domain == light.DOMAIN: if entity.domain == light.DOMAIN:
color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) or []
# Parse the request # Parse the request
parsed: dict[str, Any] = { parsed: dict[str, Any] = {
@ -765,7 +765,7 @@ def _entity_unique_id(entity_id: str) -> str:
def state_to_json(config: Config, state: State) -> dict[str, Any]: def state_to_json(config: Config, state: State) -> dict[str, Any]:
"""Convert an entity to its Hue bridge JSON representation.""" """Convert an entity to its Hue bridge JSON representation."""
color_modes = state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) color_modes = state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) or []
unique_id = _entity_unique_id(state.entity_id) unique_id = _entity_unique_id(state.entity_id)
state_dict = get_entity_state_dict(config, state) state_dict = get_entity_state_dict(config, state)

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable
from datetime import datetime, timedelta from datetime import datetime, timedelta
import itertools
import logging import logging
from typing import Any, cast from typing import Any, cast
@ -18,6 +19,7 @@ from gcal_sync.model import AccessRole, DateOrDatetime, Event
from gcal_sync.store import ScopedCalendarStore from gcal_sync.store import ScopedCalendarStore
from gcal_sync.sync import CalendarEventSyncManager from gcal_sync.sync import CalendarEventSyncManager
from gcal_sync.timeline import Timeline from gcal_sync.timeline import Timeline
from ical.iter import SortableItemValue
from homeassistant.components.calendar import ( from homeassistant.components.calendar import (
CREATE_EVENT_SCHEMA, CREATE_EVENT_SCHEMA,
@ -76,6 +78,9 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
# Maximum number of upcoming events to consider for state changes between
# coordinator updates.
MAX_UPCOMING_EVENTS = 20
# Avoid syncing super old data on initial syncs. Note that old but active # Avoid syncing super old data on initial syncs. Note that old but active
# recurring events are still included. # recurring events are still included.
@ -244,6 +249,22 @@ async def async_setup_entry(
) )
def _truncate_timeline(timeline: Timeline, max_events: int) -> Timeline:
"""Truncate the timeline to a maximum number of events.
This is used to avoid repeated expansion of recurring events during
state machine updates.
"""
upcoming = timeline.active_after(dt_util.now())
truncated = list(itertools.islice(upcoming, max_events))
return Timeline(
[
SortableItemValue(event.timespan_of(dt_util.DEFAULT_TIME_ZONE), event)
for event in truncated
]
)
class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]): class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]):
"""Coordinator for calendar RPC calls that use an efficient sync.""" """Coordinator for calendar RPC calls that use an efficient sync."""
@ -263,6 +284,7 @@ class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]):
update_interval=MIN_TIME_BETWEEN_UPDATES, update_interval=MIN_TIME_BETWEEN_UPDATES,
) )
self.sync = sync self.sync = sync
self._upcoming_timeline: Timeline | None = None
async def _async_update_data(self) -> Timeline: async def _async_update_data(self) -> Timeline:
"""Fetch data from API endpoint.""" """Fetch data from API endpoint."""
@ -271,9 +293,11 @@ class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]):
except ApiException as err: except ApiException as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err raise UpdateFailed(f"Error communicating with API: {err}") from err
return await self.sync.store_service.async_get_timeline( timeline = await self.sync.store_service.async_get_timeline(
dt_util.DEFAULT_TIME_ZONE dt_util.DEFAULT_TIME_ZONE
) )
self._upcoming_timeline = _truncate_timeline(timeline, MAX_UPCOMING_EVENTS)
return timeline
async def async_get_events( async def async_get_events(
self, start_date: datetime, end_date: datetime self, start_date: datetime, end_date: datetime
@ -291,8 +315,8 @@ class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]):
@property @property
def upcoming(self) -> Iterable[Event] | None: def upcoming(self) -> Iterable[Event] | None:
"""Return upcoming events if any.""" """Return upcoming events if any."""
if self.data: if self._upcoming_timeline:
return self.data.active_after(dt_util.now()) return self._upcoming_timeline.active_after(dt_util.now())
return None return None

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/calendar.google", "documentation": "https://www.home-assistant.io/integrations/calendar.google",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["googleapiclient"], "loggers": ["googleapiclient"],
"requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3"] "requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==6.1.1"]
} }

View File

@ -1152,12 +1152,12 @@ class TemperatureSettingTrait(_Trait):
modes = [] modes = []
attrs = self.state.attributes attrs = self.state.attributes
for mode in attrs.get(climate.ATTR_HVAC_MODES, []): for mode in attrs.get(climate.ATTR_HVAC_MODES) or []:
google_mode = self.hvac_to_google.get(mode) google_mode = self.hvac_to_google.get(mode)
if google_mode and google_mode not in modes: if google_mode and google_mode not in modes:
modes.append(google_mode) modes.append(google_mode)
for preset in attrs.get(climate.ATTR_PRESET_MODES, []): for preset in attrs.get(climate.ATTR_PRESET_MODES) or []:
google_mode = self.preset_to_google.get(preset) google_mode = self.preset_to_google.get(preset)
if google_mode and google_mode not in modes: if google_mode and google_mode not in modes:
modes.append(google_mode) modes.append(google_mode)
@ -2094,9 +2094,10 @@ class InputSelectorTrait(_Trait):
def sync_attributes(self): def sync_attributes(self):
"""Return mode attributes for a sync request.""" """Return mode attributes for a sync request."""
attrs = self.state.attributes attrs = self.state.attributes
sourcelist: list[str] = attrs.get(media_player.ATTR_INPUT_SOURCE_LIST) or []
inputs = [ inputs = [
{"key": source, "names": [{"name_synonym": [source], "lang": "en"}]} {"key": source, "names": [{"name_synonym": [source], "lang": "en"}]}
for source in attrs.get(media_player.ATTR_INPUT_SOURCE_LIST, []) for source in sourcelist
] ]
payload = {"availableInputs": inputs, "orderedInputs": True} payload = {"availableInputs": inputs, "orderedInputs": True}

View File

@ -110,7 +110,7 @@ class SetModeHandler(intent.IntentHandler):
intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes") intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes")
mode = slots["mode"]["value"] mode = slots["mode"]["value"]
if mode not in state.attributes.get(ATTR_AVAILABLE_MODES, []): if mode not in (state.attributes.get(ATTR_AVAILABLE_MODES) or []):
raise intent.IntentHandleError( raise intent.IntentHandleError(
f"Entity {state.name} does not support {mode} mode" f"Entity {state.name} does not support {mode} mode"
) )

View File

@ -26,6 +26,7 @@ from homeassistant.core import Event, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify as util_slugify from homeassistant.util import slugify as util_slugify
from homeassistant.util.ssl import get_default_context, get_default_no_verify_context
from .const import DOMAIN from .const import DOMAIN
from .coordinator import OctoprintDataUpdateCoordinator from .coordinator import OctoprintDataUpdateCoordinator
@ -159,7 +160,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
connector = aiohttp.TCPConnector( connector = aiohttp.TCPConnector(
force_close=True, force_close=True,
ssl=False if not entry.data[CONF_VERIFY_SSL] else None, ssl=get_default_no_verify_context()
if not entry.data[CONF_VERIFY_SSL]
else get_default_context(),
) )
session = aiohttp.ClientSession(connector=connector) session = aiohttp.ClientSession(connector=connector)

View File

@ -24,6 +24,7 @@ from homeassistant.const import (
) )
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.ssl import get_default_context, get_default_no_verify_context
from .const import DOMAIN from .const import DOMAIN
@ -264,7 +265,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
connector = aiohttp.TCPConnector( connector = aiohttp.TCPConnector(
force_close=True, force_close=True,
ssl=False if not verify_ssl else None, ssl=get_default_no_verify_context()
if not verify_ssl
else get_default_context(),
) )
session = aiohttp.ClientSession(connector=connector) session = aiohttp.ClientSession(connector=connector)
self._sessions.append(session) self._sessions.append(session)

View File

@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/openerz", "documentation": "https://www.home-assistant.io/integrations/openerz",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["openerz_api"], "loggers": ["openerz_api"],
"requirements": ["openerz-api==0.2.0"] "requirements": ["openerz-api==0.3.0"]
} }

View File

@ -0,0 +1,22 @@
{
"config": {
"flow_title": "[%key:component::bluetooth::config::flow_title%]",
"step": {
"user": {
"description": "[%key:component::bluetooth::config::step::user::description%]",
"data": {
"address": "[%key:common::config_flow::data::device%]"
}
},
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
}
},
"abort": {
"not_supported": "Device not supported",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}

View File

@ -5,7 +5,13 @@ from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, cast from typing import Any, cast
from asyncsleepiq import SleepIQActuator, SleepIQBed, SleepIQFootWarmer, SleepIQSleeper from asyncsleepiq import (
FootWarmingTemps,
SleepIQActuator,
SleepIQBed,
SleepIQFootWarmer,
SleepIQSleeper,
)
from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -79,6 +85,10 @@ def _get_sleeper_unique_id(bed: SleepIQBed, sleeper: SleepIQSleeper) -> str:
async def _async_set_foot_warmer_time( async def _async_set_foot_warmer_time(
foot_warmer: SleepIQFootWarmer, time: int foot_warmer: SleepIQFootWarmer, time: int
) -> None: ) -> None:
temperature = FootWarmingTemps(foot_warmer.temperature)
if temperature != FootWarmingTemps.OFF:
await foot_warmer.turn_on(temperature, time)
foot_warmer.timer = time foot_warmer.timer = time

View File

@ -17,7 +17,8 @@
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]", "oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]", "oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]" "oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
"connection_error": "Could not fetch account information. Is the user registered in the Spotify Developer Dashboard?"
}, },
"create_entry": { "create_entry": {
"default": "Successfully authenticated with Spotify." "default": "Successfully authenticated with Spotify."

View File

@ -141,52 +141,52 @@
"name": "Heating gas consumption this year" "name": "Heating gas consumption this year"
}, },
"gas_summary_consumption_heating_currentday": { "gas_summary_consumption_heating_currentday": {
"name": "Heating gas consumption current day" "name": "Heating gas consumption today"
}, },
"gas_summary_consumption_heating_currentmonth": { "gas_summary_consumption_heating_currentmonth": {
"name": "Heating gas consumption current month" "name": "Heating gas consumption this month"
}, },
"gas_summary_consumption_heating_currentyear": { "gas_summary_consumption_heating_currentyear": {
"name": "Heating gas consumption current year" "name": "Heating gas consumption this year"
}, },
"gas_summary_consumption_heating_lastsevendays": { "gas_summary_consumption_heating_lastsevendays": {
"name": "Heating gas consumption last seven days" "name": "Heating gas consumption last seven days"
}, },
"hotwater_gas_summary_consumption_heating_currentday": { "hotwater_gas_summary_consumption_heating_currentday": {
"name": "DHW gas consumption current day" "name": "DHW gas consumption today"
}, },
"hotwater_gas_summary_consumption_heating_currentmonth": { "hotwater_gas_summary_consumption_heating_currentmonth": {
"name": "DHW gas consumption current month" "name": "DHW gas consumption this month"
}, },
"hotwater_gas_summary_consumption_heating_currentyear": { "hotwater_gas_summary_consumption_heating_currentyear": {
"name": "DHW gas consumption current year" "name": "DHW gas consumption this year"
}, },
"hotwater_gas_summary_consumption_heating_lastsevendays": { "hotwater_gas_summary_consumption_heating_lastsevendays": {
"name": "DHW gas consumption last seven days" "name": "DHW gas consumption last seven days"
}, },
"energy_summary_consumption_heating_currentday": { "energy_summary_consumption_heating_currentday": {
"name": "Energy consumption of gas heating current day" "name": "Heating energy consumption today"
}, },
"energy_summary_consumption_heating_currentmonth": { "energy_summary_consumption_heating_currentmonth": {
"name": "Energy consumption of gas heating current month" "name": "Heating energy consumption this month"
}, },
"energy_summary_consumption_heating_currentyear": { "energy_summary_consumption_heating_currentyear": {
"name": "Energy consumption of gas heating current year" "name": "Heating energy consumption this year"
}, },
"energy_summary_consumption_heating_lastsevendays": { "energy_summary_consumption_heating_lastsevendays": {
"name": "Energy consumption of gas heating last seven days" "name": "Heating energy consumption last seven days"
}, },
"energy_dhw_summary_consumption_heating_currentday": { "energy_dhw_summary_consumption_heating_currentday": {
"name": "Energy consumption of hot water gas heating current day" "name": "DHW energy consumption today"
}, },
"energy_dhw_summary_consumption_heating_currentmonth": { "energy_dhw_summary_consumption_heating_currentmonth": {
"name": "Energy consumption of hot water gas heating current month" "name": "DHW energy consumption this month"
}, },
"energy_dhw_summary_consumption_heating_currentyear": { "energy_dhw_summary_consumption_heating_currentyear": {
"name": "Energy consumption of hot water gas heating current year" "name": "DHW energy consumption this year"
}, },
"energy_summary_dhw_consumption_heating_lastsevendays": { "energy_summary_dhw_consumption_heating_lastsevendays": {
"name": "Energy consumption of hot water gas heating last seven days" "name": "DHW energy consumption last seven days"
}, },
"power_production_current": { "power_production_current": {
"name": "Power production current" "name": "Power production current"

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/vodafone_station", "documentation": "https://www.home-assistant.io/integrations/vodafone_station",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aiovodafone"], "loggers": ["aiovodafone"],
"requirements": ["aiovodafone==0.4.3"] "requirements": ["aiovodafone==0.5.4"]
} }

View File

@ -6,5 +6,5 @@
"dependencies": ["auth", "application_credentials"], "dependencies": ["auth", "application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/yolink", "documentation": "https://www.home-assistant.io/integrations/yolink",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"requirements": ["yolink-api==0.3.4"] "requirements": ["yolink-api==0.3.6"]
} }

View File

@ -132,7 +132,7 @@ class Endpoint:
if not cluster_handler_class.matches(cluster, self): if not cluster_handler_class.matches(cluster, self):
cluster_handler_class = ClusterHandler cluster_handler_class = ClusterHandler
_LOGGER.info( _LOGGER.debug(
"Creating cluster handler for cluster id: %s class: %s", "Creating cluster handler for cluster id: %s class: %s",
cluster_id, cluster_id,
cluster_handler_class, cluster_handler_class,
@ -199,11 +199,11 @@ class Endpoint:
results = await gather(*tasks, return_exceptions=True) results = await gather(*tasks, return_exceptions=True)
for cluster_handler, outcome in zip(cluster_handlers, results): for cluster_handler, outcome in zip(cluster_handlers, results):
if isinstance(outcome, Exception): if isinstance(outcome, Exception):
cluster_handler.warning( cluster_handler.debug(
"'%s' stage failed: %s", func_name, str(outcome), exc_info=outcome "'%s' stage failed: %s", func_name, str(outcome), exc_info=outcome
) )
continue else:
cluster_handler.debug("'%s' stage succeeded", func_name) cluster_handler.debug("'%s' stage succeeded", func_name)
def async_new_entity( def async_new_entity(
self, self,

View File

@ -26,7 +26,7 @@
"pyserial-asyncio==0.6", "pyserial-asyncio==0.6",
"zha-quirks==0.0.109", "zha-quirks==0.0.109",
"zigpy-deconz==0.22.4", "zigpy-deconz==0.22.4",
"zigpy==0.60.6", "zigpy==0.60.7",
"zigpy-xbee==0.20.1", "zigpy-xbee==0.20.1",
"zigpy-zigate==0.12.0", "zigpy-zigate==0.12.0",
"zigpy-znp==0.12.1", "zigpy-znp==0.12.1",

View File

@ -481,8 +481,12 @@ class Illuminance(Sensor):
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = LIGHT_LUX _attr_native_unit_of_measurement = LIGHT_LUX
def formatter(self, value: int) -> int: def formatter(self, value: int) -> int | None:
"""Convert illumination data.""" """Convert illumination data."""
if value == 0:
return 0
if value == 0xFFFF:
return None
return round(pow(10, ((value - 1) / 10000))) return round(pow(10, ((value - 1) / 10000)))

View File

@ -16,7 +16,7 @@ from .helpers.deprecation import (
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024 MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 1 MINOR_VERSION: Final = 1
PATCH_VERSION: Final = "5" PATCH_VERSION: Final = "6"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)

View File

@ -3,7 +3,7 @@
aiodiscover==1.6.0 aiodiscover==1.6.0
aiohttp-fast-url-dispatcher==0.3.0 aiohttp-fast-url-dispatcher==0.3.0
aiohttp-zlib-ng==0.1.3 aiohttp-zlib-ng==0.1.3
aiohttp==3.9.1 aiohttp==3.9.3
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
astral==2.2 astral==2.2
async-upnp-client==0.38.1 async-upnp-client==0.38.1

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2024.1.5" version = "2024.1.6"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"
@ -23,7 +23,7 @@ classifiers = [
] ]
requires-python = ">=3.11.0" requires-python = ">=3.11.0"
dependencies = [ dependencies = [
"aiohttp==3.9.1", "aiohttp==3.9.3",
"aiohttp_cors==0.7.0", "aiohttp_cors==0.7.0",
"aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-fast-url-dispatcher==0.3.0",
"aiohttp-zlib-ng==0.1.3", "aiohttp-zlib-ng==0.1.3",

View File

@ -3,7 +3,7 @@
-c homeassistant/package_constraints.txt -c homeassistant/package_constraints.txt
# Home Assistant Core # Home Assistant Core
aiohttp==3.9.1 aiohttp==3.9.3
aiohttp_cors==0.7.0 aiohttp_cors==0.7.0
aiohttp-fast-url-dispatcher==0.3.0 aiohttp-fast-url-dispatcher==0.3.0
aiohttp-zlib-ng==0.1.3 aiohttp-zlib-ng==0.1.3

View File

@ -383,7 +383,7 @@ aiounifi==69
aiovlc==0.1.0 aiovlc==0.1.0
# homeassistant.components.vodafone_station # homeassistant.components.vodafone_station
aiovodafone==0.4.3 aiovodafone==0.5.4
# homeassistant.components.waqi # homeassistant.components.waqi
aiowaqi==3.0.1 aiowaqi==3.0.1
@ -404,7 +404,7 @@ aioymaps==1.2.2
airly==1.1.0 airly==1.1.0
# homeassistant.components.airthings_ble # homeassistant.components.airthings_ble
airthings-ble==0.5.6-2 airthings-ble==0.6.0
# homeassistant.components.airthings # homeassistant.components.airthings
airthings-cloud==0.1.0 airthings-cloud==0.1.0
@ -1076,6 +1076,7 @@ ibeacon-ble==1.0.1
# homeassistant.components.watson_iot # homeassistant.components.watson_iot
ibmiotf==0.3.4 ibmiotf==0.3.4
# homeassistant.components.google
# homeassistant.components.local_calendar # homeassistant.components.local_calendar
# homeassistant.components.local_todo # homeassistant.components.local_todo
ical==6.1.1 ical==6.1.1
@ -1413,7 +1414,7 @@ openai==1.3.8
# opencv-python-headless==4.6.0.66 # opencv-python-headless==4.6.0.66
# homeassistant.components.openerz # homeassistant.components.openerz
openerz-api==0.2.0 openerz-api==0.3.0
# homeassistant.components.openevse # homeassistant.components.openevse
openevsewifi==1.1.2 openevsewifi==1.1.2
@ -2842,7 +2843,7 @@ yeelight==0.7.14
yeelightsunflower==0.0.10 yeelightsunflower==0.0.10
# homeassistant.components.yolink # homeassistant.components.yolink
yolink-api==0.3.4 yolink-api==0.3.6
# homeassistant.components.youless # homeassistant.components.youless
youless-api==1.0.1 youless-api==1.0.1
@ -2887,7 +2888,7 @@ zigpy-zigate==0.12.0
zigpy-znp==0.12.1 zigpy-znp==0.12.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.60.6 zigpy==0.60.7
# homeassistant.components.zoneminder # homeassistant.components.zoneminder
zm-py==0.5.4 zm-py==0.5.4

View File

@ -356,7 +356,7 @@ aiounifi==69
aiovlc==0.1.0 aiovlc==0.1.0
# homeassistant.components.vodafone_station # homeassistant.components.vodafone_station
aiovodafone==0.4.3 aiovodafone==0.5.4
# homeassistant.components.waqi # homeassistant.components.waqi
aiowaqi==3.0.1 aiowaqi==3.0.1
@ -377,7 +377,7 @@ aioymaps==1.2.2
airly==1.1.0 airly==1.1.0
# homeassistant.components.airthings_ble # homeassistant.components.airthings_ble
airthings-ble==0.5.6-2 airthings-ble==0.6.0
# homeassistant.components.airthings # homeassistant.components.airthings
airthings-cloud==0.1.0 airthings-cloud==0.1.0
@ -860,6 +860,7 @@ iaqualink==0.5.0
# homeassistant.components.ibeacon # homeassistant.components.ibeacon
ibeacon-ble==1.0.1 ibeacon-ble==1.0.1
# homeassistant.components.google
# homeassistant.components.local_calendar # homeassistant.components.local_calendar
# homeassistant.components.local_todo # homeassistant.components.local_todo
ical==6.1.1 ical==6.1.1
@ -1110,7 +1111,7 @@ open-meteo==0.3.1
openai==1.3.8 openai==1.3.8
# homeassistant.components.openerz # homeassistant.components.openerz
openerz-api==0.2.0 openerz-api==0.3.0
# homeassistant.components.openhome # homeassistant.components.openhome
openhomedevice==2.2.0 openhomedevice==2.2.0
@ -2150,7 +2151,7 @@ yalexs==1.10.0
yeelight==0.7.14 yeelight==0.7.14
# homeassistant.components.yolink # homeassistant.components.yolink
yolink-api==0.3.4 yolink-api==0.3.6
# homeassistant.components.youless # homeassistant.components.youless
youless-api==1.0.1 youless-api==1.0.1
@ -2186,7 +2187,7 @@ zigpy-zigate==0.12.0
zigpy-znp==0.12.1 zigpy-znp==0.12.1
# homeassistant.components.zha # homeassistant.components.zha
zigpy==0.60.6 zigpy==0.60.7
# homeassistant.components.zwave_js # homeassistant.components.zwave_js
zwave-js-server-python==0.55.3 zwave-js-server-python==0.55.3

View File

@ -188,7 +188,10 @@ async def test_intent_set_mode_tests_feature(hass: HomeAssistant) -> None:
assert len(mode_calls) == 0 assert len(mode_calls) == 0
async def test_intent_set_unknown_mode(hass: HomeAssistant) -> None: @pytest.mark.parametrize("available_modes", (["home", "away"], None))
async def test_intent_set_unknown_mode(
hass: HomeAssistant, available_modes: list[str] | None
) -> None:
"""Test the set mode intent for unsupported mode.""" """Test the set mode intent for unsupported mode."""
hass.states.async_set( hass.states.async_set(
"humidifier.bedroom_humidifier", "humidifier.bedroom_humidifier",
@ -196,8 +199,8 @@ async def test_intent_set_unknown_mode(hass: HomeAssistant) -> None:
{ {
ATTR_HUMIDITY: 40, ATTR_HUMIDITY: 40,
ATTR_SUPPORTED_FEATURES: 1, ATTR_SUPPORTED_FEATURES: 1,
ATTR_AVAILABLE_MODES: ["home", "away"], ATTR_AVAILABLE_MODES: available_modes,
ATTR_MODE: "home", ATTR_MODE: None,
}, },
) )
mode_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_MODE) mode_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_MODE)

View File

@ -220,7 +220,7 @@ async def test_auth_close_after_revoke(
await hass.auth.async_remove_refresh_token(refresh_token) await hass.auth.async_remove_refresh_token(refresh_token)
msg = await websocket_client.receive() msg = await websocket_client.receive()
assert msg.type == aiohttp.WSMsgType.CLOSE assert msg.type == aiohttp.WSMsgType.CLOSED
assert websocket_client.closed assert websocket_client.closed

View File

@ -42,7 +42,7 @@ async def test_pending_msg_overflow(
for idx in range(10): for idx in range(10):
await websocket_client.send_json({"id": idx + 1, "type": "ping"}) await websocket_client.send_json({"id": idx + 1, "type": "ping"})
msg = await websocket_client.receive() msg = await websocket_client.receive()
assert msg.type == WSMsgType.close assert msg.type == WSMsgType.CLOSED
async def test_cleanup_on_cancellation( async def test_cleanup_on_cancellation(
@ -248,7 +248,7 @@ async def test_pending_msg_peak(
) )
msg = await websocket_client.receive() msg = await websocket_client.receive()
assert msg.type == WSMsgType.close assert msg.type == WSMsgType.CLOSED
assert "Client unable to keep up with pending messages" in caplog.text assert "Client unable to keep up with pending messages" in caplog.text
assert "Stayed over 5 for 5 seconds" in caplog.text assert "Stayed over 5 for 5 seconds" in caplog.text
assert "overload" in caplog.text assert "overload" in caplog.text
@ -296,7 +296,7 @@ async def test_pending_msg_peak_recovery(
msg = await websocket_client.receive() msg = await websocket_client.receive()
assert msg.type == WSMsgType.TEXT assert msg.type == WSMsgType.TEXT
msg = await websocket_client.receive() msg = await websocket_client.receive()
assert msg.type == WSMsgType.close assert msg.type == WSMsgType.CLOSED
assert "Client unable to keep up with pending messages" not in caplog.text assert "Client unable to keep up with pending messages" not in caplog.text

View File

@ -40,7 +40,7 @@ async def test_quiting_hass(hass: HomeAssistant, websocket_client) -> None:
msg = await websocket_client.receive() msg = await websocket_client.receive()
assert msg.type == WSMsgType.CLOSE assert msg.type == WSMsgType.CLOSED
async def test_unknown_command(websocket_client) -> None: async def test_unknown_command(websocket_client) -> None:

View File

@ -591,8 +591,8 @@ async def test_ep_cluster_handlers_configure(cluster_handler) -> None:
assert ch.async_configure.call_count == 1 assert ch.async_configure.call_count == 1
assert ch.async_configure.await_count == 1 assert ch.async_configure.await_count == 1
assert ch_3.warning.call_count == 2 assert ch_3.debug.call_count == 2
assert ch_5.warning.call_count == 2 assert ch_5.debug.call_count == 2
async def test_poll_control_configure(poll_control_ch) -> None: async def test_poll_control_configure(poll_control_ch) -> None:

View File

@ -136,6 +136,12 @@ async def async_test_illuminance(hass, cluster, entity_id):
await send_attributes_report(hass, cluster, {1: 1, 0: 10, 2: 20}) await send_attributes_report(hass, cluster, {1: 1, 0: 10, 2: 20})
assert_state(hass, entity_id, "1", LIGHT_LUX) assert_state(hass, entity_id, "1", LIGHT_LUX)
await send_attributes_report(hass, cluster, {1: 0, 0: 0, 2: 20})
assert_state(hass, entity_id, "0", LIGHT_LUX)
await send_attributes_report(hass, cluster, {1: 0, 0: 0xFFFF, 2: 20})
assert_state(hass, entity_id, "unknown", LIGHT_LUX)
async def async_test_metering(hass, cluster, entity_id): async def async_test_metering(hass, cluster, entity_id):
"""Test Smart Energy metering sensor.""" """Test Smart Energy metering sensor."""