mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 22:57:17 +00:00
2024.2.3 (#111133)
This commit is contained in:
commit
1ee39275fc
@ -11,5 +11,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairzone"],
|
"loggers": ["aioairzone"],
|
||||||
"requirements": ["aioairzone==0.7.2"]
|
"requirements": ["aioairzone==0.7.4"]
|
||||||
}
|
}
|
||||||
|
@ -52,8 +52,10 @@ def get_service(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Ordered list of URLs
|
# Ordered list of URLs
|
||||||
if config.get(CONF_URL) and not a_obj.add(config[CONF_URL]):
|
if urls := config.get(CONF_URL):
|
||||||
_LOGGER.error("Invalid Apprise URL(s) supplied")
|
for entry in urls:
|
||||||
|
if not a_obj.add(entry):
|
||||||
|
_LOGGER.error("One or more specified Apprise URL(s) are invalid")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return AppriseNotificationService(a_obj)
|
return AppriseNotificationService(a_obj)
|
||||||
|
@ -68,13 +68,22 @@ async def _async_reproduce_states(
|
|||||||
[ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW],
|
[ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW],
|
||||||
)
|
)
|
||||||
|
|
||||||
if ATTR_PRESET_MODE in state.attributes:
|
if (
|
||||||
|
ATTR_PRESET_MODE in state.attributes
|
||||||
|
and state.attributes[ATTR_PRESET_MODE] is not None
|
||||||
|
):
|
||||||
await call_service(SERVICE_SET_PRESET_MODE, [ATTR_PRESET_MODE])
|
await call_service(SERVICE_SET_PRESET_MODE, [ATTR_PRESET_MODE])
|
||||||
|
|
||||||
if ATTR_SWING_MODE in state.attributes:
|
if (
|
||||||
|
ATTR_SWING_MODE in state.attributes
|
||||||
|
and state.attributes[ATTR_SWING_MODE] is not None
|
||||||
|
):
|
||||||
await call_service(SERVICE_SET_SWING_MODE, [ATTR_SWING_MODE])
|
await call_service(SERVICE_SET_SWING_MODE, [ATTR_SWING_MODE])
|
||||||
|
|
||||||
if ATTR_FAN_MODE in state.attributes:
|
if (
|
||||||
|
ATTR_FAN_MODE in state.attributes
|
||||||
|
and state.attributes[ATTR_FAN_MODE] is not None
|
||||||
|
):
|
||||||
await call_service(SERVICE_SET_FAN_MODE, [ATTR_FAN_MODE])
|
await call_service(SERVICE_SET_FAN_MODE, [ATTR_FAN_MODE])
|
||||||
|
|
||||||
if ATTR_HUMIDITY in state.attributes:
|
if ATTR_HUMIDITY in state.attributes:
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["deluge_client"],
|
"loggers": ["deluge_client"],
|
||||||
"requirements": ["deluge-client==1.7.1"]
|
"requirements": ["deluge-client==1.10.2"]
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||||
"requirements": ["py-sucks==0.9.9", "deebot-client==5.2.1"]
|
"requirements": ["py-sucks==0.9.9", "deebot-client==5.2.2"]
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ class EcovacsLegacyVacuum(StateVacuumEntity):
|
|||||||
This will not change the entity's state. If the error caused the state
|
This will not change the entity's state. If the error caused the state
|
||||||
to change, that will come through as a separate on_status event
|
to change, that will come through as a separate on_status event
|
||||||
"""
|
"""
|
||||||
if error == "no_error":
|
if error in ["no_error", sucks.ERROR_CODES["100"]]:
|
||||||
self.error = None
|
self.error = None
|
||||||
else:
|
else:
|
||||||
self.error = error
|
self.error = error
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
"""Support for Enigma2 media players."""
|
"""Support for Enigma2 media players."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from aiohttp.client_exceptions import ClientConnectorError
|
import contextlib
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
from aiohttp.client_exceptions import ClientConnectorError, ServerDisconnectedError
|
||||||
from openwebif.api import OpenWebIfDevice
|
from openwebif.api import OpenWebIfDevice
|
||||||
from openwebif.enums import RemoteControlCodes, SetVolumeOption
|
from openwebif.enums import PowerState, RemoteControlCodes, SetVolumeOption
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from yarl import URL
|
from yarl import URL
|
||||||
|
|
||||||
@ -50,6 +53,8 @@ ATTR_MEDIA_DESCRIPTION = "media_description"
|
|||||||
ATTR_MEDIA_END_TIME = "media_end_time"
|
ATTR_MEDIA_END_TIME = "media_end_time"
|
||||||
ATTR_MEDIA_START_TIME = "media_start_time"
|
ATTR_MEDIA_START_TIME = "media_start_time"
|
||||||
|
|
||||||
|
_LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
@ -143,7 +148,12 @@ class Enigma2Device(MediaPlayerEntity):
|
|||||||
|
|
||||||
async def async_turn_off(self) -> None:
|
async def async_turn_off(self) -> None:
|
||||||
"""Turn off media player."""
|
"""Turn off media player."""
|
||||||
await self._device.turn_off()
|
if self._device.turn_off_to_deep:
|
||||||
|
with contextlib.suppress(ServerDisconnectedError):
|
||||||
|
await self._device.set_powerstate(PowerState.DEEP_STANDBY)
|
||||||
|
self._attr_available = False
|
||||||
|
else:
|
||||||
|
await self._device.set_powerstate(PowerState.STANDBY)
|
||||||
|
|
||||||
async def async_turn_on(self) -> None:
|
async def async_turn_on(self) -> None:
|
||||||
"""Turn the media player on."""
|
"""Turn the media player on."""
|
||||||
@ -191,8 +201,19 @@ class Enigma2Device(MediaPlayerEntity):
|
|||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update state of the media_player."""
|
"""Update state of the media_player."""
|
||||||
|
try:
|
||||||
await self._device.update()
|
await self._device.update()
|
||||||
self._attr_available = not self._device.is_offline
|
except ClientConnectorError as err:
|
||||||
|
if self._attr_available:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"%s is unavailable. Error: %s", self._device.base.host, err
|
||||||
|
)
|
||||||
|
self._attr_available = False
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._attr_available:
|
||||||
|
_LOGGER.debug("%s is available", self._device.base.host)
|
||||||
|
self._attr_available = True
|
||||||
|
|
||||||
if not self._device.status.in_standby:
|
if not self._device.status.in_standby:
|
||||||
self._attr_extra_state_attributes = {
|
self._attr_extra_state_attributes = {
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"dependencies": ["network"],
|
"dependencies": ["network"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["govee-local-api==1.4.1"]
|
"requirements": ["govee-local-api==1.4.4"]
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["holidays==0.42", "babel==2.13.1"]
|
"requirements": ["holidays==0.43", "babel==2.13.1"]
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/html5",
|
"documentation": "https://www.home-assistant.io/integrations/html5",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["http_ece", "py_vapid", "pywebpush"],
|
"loggers": ["http_ece", "py_vapid", "pywebpush"],
|
||||||
"requirements": ["pywebpush==1.9.2"]
|
"requirements": ["pywebpush==1.14.1"]
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,8 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.helpers.device_registry as dr
|
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
@ -186,6 +186,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
lutron_client.connect()
|
lutron_client.connect()
|
||||||
_LOGGER.info("Connected to main repeater at %s", host)
|
_LOGGER.info("Connected to main repeater at %s", host)
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
entry_data = LutronData(
|
entry_data = LutronData(
|
||||||
client=lutron_client,
|
client=lutron_client,
|
||||||
binary_sensors=[],
|
binary_sensors=[],
|
||||||
@ -201,17 +204,39 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
for area in lutron_client.areas:
|
for area in lutron_client.areas:
|
||||||
_LOGGER.debug("Working on area %s", area.name)
|
_LOGGER.debug("Working on area %s", area.name)
|
||||||
for output in area.outputs:
|
for output in area.outputs:
|
||||||
|
platform = None
|
||||||
_LOGGER.debug("Working on output %s", output.type)
|
_LOGGER.debug("Working on output %s", output.type)
|
||||||
if output.type == "SYSTEM_SHADE":
|
if output.type == "SYSTEM_SHADE":
|
||||||
entry_data.covers.append((area.name, output))
|
entry_data.covers.append((area.name, output))
|
||||||
|
platform = Platform.COVER
|
||||||
elif output.type == "CEILING_FAN_TYPE":
|
elif output.type == "CEILING_FAN_TYPE":
|
||||||
entry_data.fans.append((area.name, output))
|
entry_data.fans.append((area.name, output))
|
||||||
|
platform = Platform.FAN
|
||||||
# Deprecated, should be removed in 2024.8
|
# Deprecated, should be removed in 2024.8
|
||||||
entry_data.lights.append((area.name, output))
|
entry_data.lights.append((area.name, output))
|
||||||
elif output.is_dimmable:
|
elif output.is_dimmable:
|
||||||
entry_data.lights.append((area.name, output))
|
entry_data.lights.append((area.name, output))
|
||||||
|
platform = Platform.LIGHT
|
||||||
else:
|
else:
|
||||||
entry_data.switches.append((area.name, output))
|
entry_data.switches.append((area.name, output))
|
||||||
|
platform = Platform.SWITCH
|
||||||
|
|
||||||
|
_async_check_entity_unique_id(
|
||||||
|
hass,
|
||||||
|
entity_registry,
|
||||||
|
platform,
|
||||||
|
output.uuid,
|
||||||
|
output.legacy_uuid,
|
||||||
|
entry_data.client.guid,
|
||||||
|
)
|
||||||
|
_async_check_device_identifiers(
|
||||||
|
hass,
|
||||||
|
device_registry,
|
||||||
|
output.uuid,
|
||||||
|
output.legacy_uuid,
|
||||||
|
entry_data.client.guid,
|
||||||
|
)
|
||||||
|
|
||||||
for keypad in area.keypads:
|
for keypad in area.keypads:
|
||||||
for button in keypad.buttons:
|
for button in keypad.buttons:
|
||||||
# If the button has a function assigned to it, add it as a scene
|
# If the button has a function assigned to it, add it as a scene
|
||||||
@ -228,11 +253,46 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
)
|
)
|
||||||
entry_data.scenes.append((area.name, keypad, button, led))
|
entry_data.scenes.append((area.name, keypad, button, led))
|
||||||
|
|
||||||
|
platform = Platform.SCENE
|
||||||
|
_async_check_entity_unique_id(
|
||||||
|
hass,
|
||||||
|
entity_registry,
|
||||||
|
platform,
|
||||||
|
button.uuid,
|
||||||
|
button.legacy_uuid,
|
||||||
|
entry_data.client.guid,
|
||||||
|
)
|
||||||
|
if led is not None:
|
||||||
|
platform = Platform.SWITCH
|
||||||
|
_async_check_entity_unique_id(
|
||||||
|
hass,
|
||||||
|
entity_registry,
|
||||||
|
platform,
|
||||||
|
led.uuid,
|
||||||
|
led.legacy_uuid,
|
||||||
|
entry_data.client.guid,
|
||||||
|
)
|
||||||
|
|
||||||
entry_data.buttons.append(LutronButton(hass, area.name, keypad, button))
|
entry_data.buttons.append(LutronButton(hass, area.name, keypad, button))
|
||||||
if area.occupancy_group is not None:
|
if area.occupancy_group is not None:
|
||||||
entry_data.binary_sensors.append((area.name, area.occupancy_group))
|
entry_data.binary_sensors.append((area.name, area.occupancy_group))
|
||||||
|
platform = Platform.BINARY_SENSOR
|
||||||
|
_async_check_entity_unique_id(
|
||||||
|
hass,
|
||||||
|
entity_registry,
|
||||||
|
platform,
|
||||||
|
area.occupancy_group.uuid,
|
||||||
|
area.occupancy_group.legacy_uuid,
|
||||||
|
entry_data.client.guid,
|
||||||
|
)
|
||||||
|
_async_check_device_identifiers(
|
||||||
|
hass,
|
||||||
|
device_registry,
|
||||||
|
area.occupancy_group.uuid,
|
||||||
|
area.occupancy_group.legacy_uuid,
|
||||||
|
entry_data.client.guid,
|
||||||
|
)
|
||||||
|
|
||||||
device_registry = dr.async_get(hass)
|
|
||||||
device_registry.async_get_or_create(
|
device_registry.async_get_or_create(
|
||||||
config_entry_id=config_entry.entry_id,
|
config_entry_id=config_entry.entry_id,
|
||||||
identifiers={(DOMAIN, lutron_client.guid)},
|
identifiers={(DOMAIN, lutron_client.guid)},
|
||||||
@ -247,6 +307,52 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _async_check_entity_unique_id(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
platform: str,
|
||||||
|
uuid: str,
|
||||||
|
legacy_uuid: str,
|
||||||
|
controller_guid: str,
|
||||||
|
) -> None:
|
||||||
|
"""If uuid becomes available update to use it."""
|
||||||
|
|
||||||
|
if not uuid:
|
||||||
|
return
|
||||||
|
|
||||||
|
unique_id = f"{controller_guid}_{legacy_uuid}"
|
||||||
|
entity_id = entity_registry.async_get_entity_id(
|
||||||
|
domain=platform, platform=DOMAIN, unique_id=unique_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if entity_id:
|
||||||
|
new_unique_id = f"{controller_guid}_{uuid}"
|
||||||
|
_LOGGER.debug("Updating entity id from %s to %s", unique_id, new_unique_id)
|
||||||
|
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _async_check_device_identifiers(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
uuid: str,
|
||||||
|
legacy_uuid: str,
|
||||||
|
controller_guid: str,
|
||||||
|
) -> None:
|
||||||
|
"""If uuid becomes available update to use it."""
|
||||||
|
|
||||||
|
if not uuid:
|
||||||
|
return
|
||||||
|
|
||||||
|
unique_id = f"{controller_guid}_{legacy_uuid}"
|
||||||
|
device = device_registry.async_get_device(identifiers={(DOMAIN, unique_id)})
|
||||||
|
if device:
|
||||||
|
new_unique_id = f"{controller_guid}_{uuid}"
|
||||||
|
_LOGGER.debug("Updating device id from %s to %s", unique_id, new_unique_id)
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device.id, new_identifiers={(DOMAIN, new_unique_id)}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Clean up resources and entities associated with the integration."""
|
"""Clean up resources and entities associated with the integration."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
@ -41,11 +41,11 @@ class LutronBaseEntity(Entity):
|
|||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> str | None:
|
def unique_id(self) -> str:
|
||||||
"""Return a unique ID."""
|
"""Return a unique ID."""
|
||||||
# Temporary fix for https://github.com/thecynic/pylutron/issues/70
|
|
||||||
if self._lutron_device.uuid is None:
|
if self._lutron_device.uuid is None:
|
||||||
return None
|
return f"{self._controller.guid}_{self._lutron_device.legacy_uuid}"
|
||||||
return f"{self._controller.guid}_{self._lutron_device.uuid}"
|
return f"{self._controller.guid}_{self._lutron_device.uuid}"
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
@ -63,7 +63,7 @@ class LutronDevice(LutronBaseEntity):
|
|||||||
"""Initialize the device."""
|
"""Initialize the device."""
|
||||||
super().__init__(area_name, lutron_device, controller)
|
super().__init__(area_name, lutron_device, controller)
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, lutron_device.uuid)},
|
identifiers={(DOMAIN, self.unique_id)},
|
||||||
manufacturer="Lutron",
|
manufacturer="Lutron",
|
||||||
name=lutron_device.name,
|
name=lutron_device.name,
|
||||||
suggested_area=area_name,
|
suggested_area=area_name,
|
||||||
|
@ -150,5 +150,5 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|||||||
await store.async_save(savable_state(hass))
|
await store.async_save(savable_state(hass))
|
||||||
|
|
||||||
if CONF_CLOUDHOOK_URL in entry.data:
|
if CONF_CLOUDHOOK_URL in entry.data:
|
||||||
with suppress(cloud.CloudNotAvailable):
|
with suppress(cloud.CloudNotAvailable, ValueError):
|
||||||
await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID])
|
await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID])
|
||||||
|
@ -21,5 +21,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["motionblinds"],
|
"loggers": ["motionblinds"],
|
||||||
"requirements": ["motionblinds==0.6.20"]
|
"requirements": ["motionblinds==0.6.21"]
|
||||||
}
|
}
|
||||||
|
@ -354,9 +354,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
self._user = self._reauth_entry.data[CONF_USERNAME]
|
self._user = self._reauth_entry.data[CONF_USERNAME]
|
||||||
self._server = self._reauth_entry.data[CONF_HUB]
|
self._server = self._reauth_entry.data[CONF_HUB]
|
||||||
self._api_type = self._reauth_entry.data[CONF_API_TYPE]
|
self._api_type = self._reauth_entry.data.get(CONF_API_TYPE, APIType.CLOUD)
|
||||||
|
|
||||||
if self._reauth_entry.data[CONF_API_TYPE] == APIType.LOCAL:
|
if self._api_type == APIType.LOCAL:
|
||||||
self._host = self._reauth_entry.data[CONF_HOST]
|
self._host = self._reauth_entry.data[CONF_HOST]
|
||||||
|
|
||||||
return await self.async_step_user(dict(entry_data))
|
return await self.async_step_user(dict(entry_data))
|
||||||
|
@ -100,10 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
|
async with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
|
||||||
try:
|
try:
|
||||||
return await host.api.check_new_firmware()
|
return await host.api.check_new_firmware()
|
||||||
except (ReolinkError, asyncio.exceptions.CancelledError) as err:
|
except ReolinkError as err:
|
||||||
task = asyncio.current_task()
|
|
||||||
if task is not None:
|
|
||||||
task.uncancel()
|
|
||||||
if starting:
|
if starting:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Error checking Reolink firmware update at startup "
|
"Error checking Reolink firmware update at startup "
|
||||||
@ -133,15 +130,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
update_interval=FIRMWARE_UPDATE_INTERVAL,
|
update_interval=FIRMWARE_UPDATE_INTERVAL,
|
||||||
)
|
)
|
||||||
# Fetch initial data so we have data when entities subscribe
|
# Fetch initial data so we have data when entities subscribe
|
||||||
try:
|
results = await asyncio.gather(
|
||||||
# If camera WAN blocked, firmware check fails, do not prevent setup
|
|
||||||
await asyncio.gather(
|
|
||||||
device_coordinator.async_config_entry_first_refresh(),
|
device_coordinator.async_config_entry_first_refresh(),
|
||||||
firmware_coordinator.async_config_entry_first_refresh(),
|
firmware_coordinator.async_config_entry_first_refresh(),
|
||||||
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
except ConfigEntryNotReady:
|
# If camera WAN blocked, firmware check fails, do not prevent setup
|
||||||
|
# so don't check firmware_coordinator exceptions
|
||||||
|
if isinstance(results[0], BaseException):
|
||||||
await host.stop()
|
await host.stop()
|
||||||
raise
|
raise results[0]
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData(
|
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData(
|
||||||
host=host,
|
host=host,
|
||||||
|
@ -18,5 +18,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["reolink_aio"],
|
"loggers": ["reolink_aio"],
|
||||||
"requirements": ["reolink-aio==0.8.7"]
|
"requirements": ["reolink-aio==0.8.8"]
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["rokuecp"],
|
"loggers": ["rokuecp"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["rokuecp==0.19.0"],
|
"requirements": ["rokuecp==0.19.1"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"st": "roku:ecp",
|
"st": "roku:ecp",
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/roomba",
|
"documentation": "https://www.home-assistant.io/integrations/roomba",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["paho_mqtt", "roombapy"],
|
"loggers": ["paho_mqtt", "roombapy"],
|
||||||
"requirements": ["roombapy==1.6.10"],
|
"requirements": ["roombapy==1.6.12"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_amzn-alexa._tcp.local.",
|
"type": "_amzn-alexa._tcp.local.",
|
||||||
|
@ -12,6 +12,7 @@ from aiotankerkoenig import (
|
|||||||
TankerkoenigConnectionError,
|
TankerkoenigConnectionError,
|
||||||
TankerkoenigError,
|
TankerkoenigError,
|
||||||
TankerkoenigInvalidKeyError,
|
TankerkoenigInvalidKeyError,
|
||||||
|
TankerkoenigRateLimitError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -19,7 +20,7 @@ from homeassistant.const import CONF_API_KEY, CONF_SHOW_ON_MAP
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import CONF_FUEL_TYPES, CONF_STATIONS
|
from .const import CONF_FUEL_TYPES, CONF_STATIONS
|
||||||
|
|
||||||
@ -78,13 +79,22 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
station_ids = list(self.stations)
|
station_ids = list(self.stations)
|
||||||
|
|
||||||
prices = {}
|
prices = {}
|
||||||
|
|
||||||
# The API seems to only return at most 10 results, so split the list in chunks of 10
|
# The API seems to only return at most 10 results, so split the list in chunks of 10
|
||||||
# and merge it together.
|
# and merge it together.
|
||||||
for index in range(ceil(len(station_ids) / 10)):
|
for index in range(ceil(len(station_ids) / 10)):
|
||||||
|
try:
|
||||||
data = await self._tankerkoenig.prices(
|
data = await self._tankerkoenig.prices(
|
||||||
station_ids[index * 10 : (index + 1) * 10]
|
station_ids[index * 10 : (index + 1) * 10]
|
||||||
)
|
)
|
||||||
|
except TankerkoenigInvalidKeyError as err:
|
||||||
|
raise ConfigEntryAuthFailed(err) from err
|
||||||
|
except (TankerkoenigError, TankerkoenigConnectionError) as err:
|
||||||
|
if isinstance(err, TankerkoenigRateLimitError):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"API rate limit reached, consider to increase polling interval"
|
||||||
|
)
|
||||||
|
raise UpdateFailed(err) from err
|
||||||
|
|
||||||
prices.update(data)
|
prices.update(data)
|
||||||
|
|
||||||
return prices
|
return prices
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/tankerkoenig",
|
"documentation": "https://www.home-assistant.io/integrations/tankerkoenig",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aiotankerkoenig"],
|
"loggers": ["aiotankerkoenig"],
|
||||||
"requirements": ["aiotankerkoenig==0.3.0"]
|
"requirements": ["aiotankerkoenig==0.4.1"]
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from tesla_fleet_api.exceptions import TeslaFleetError
|
||||||
|
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
@ -45,11 +48,22 @@ class TeslemetryVehicleEntity(CoordinatorEntity[TeslemetryVehicleDataCoordinator
|
|||||||
async def wake_up_if_asleep(self) -> None:
|
async def wake_up_if_asleep(self) -> None:
|
||||||
"""Wake up the vehicle if its asleep."""
|
"""Wake up the vehicle if its asleep."""
|
||||||
async with self._wakelock:
|
async with self._wakelock:
|
||||||
|
times = 0
|
||||||
while self.coordinator.data["state"] != TeslemetryState.ONLINE:
|
while self.coordinator.data["state"] != TeslemetryState.ONLINE:
|
||||||
state = (await self.api.wake_up())["response"]["state"]
|
try:
|
||||||
|
if times == 0:
|
||||||
|
cmd = await self.api.wake_up()
|
||||||
|
else:
|
||||||
|
cmd = await self.api.vehicle()
|
||||||
|
state = cmd["response"]["state"]
|
||||||
|
except TeslaFleetError as e:
|
||||||
|
raise HomeAssistantError(str(e)) from e
|
||||||
self.coordinator.data["state"] = state
|
self.coordinator.data["state"] = state
|
||||||
if state != TeslemetryState.ONLINE:
|
if state != TeslemetryState.ONLINE:
|
||||||
await asyncio.sleep(5)
|
times += 1
|
||||||
|
if times >= 4: # Give up after 30 seconds total
|
||||||
|
raise HomeAssistantError("Could not wake up vehicle")
|
||||||
|
await asyncio.sleep(times * 5)
|
||||||
|
|
||||||
def get(self, key: str | None = None, default: Any | None = None) -> Any:
|
def get(self, key: str | None = None, default: Any | None = None) -> Any:
|
||||||
"""Return a specific value from coordinator data."""
|
"""Return a specific value from coordinator data."""
|
||||||
|
@ -11,6 +11,7 @@ from tessie_api import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
|
ATTR_HVAC_MODE,
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
@ -112,7 +113,10 @@ class TessieClimateEntity(TessieEntity, ClimateEntity):
|
|||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set the climate temperature."""
|
"""Set the climate temperature."""
|
||||||
temp = kwargs[ATTR_TEMPERATURE]
|
if mode := kwargs.get(ATTR_HVAC_MODE):
|
||||||
|
await self.async_set_hvac_mode(mode)
|
||||||
|
|
||||||
|
if temp := kwargs.get(ATTR_TEMPERATURE):
|
||||||
await self.run(set_temperature, temperature=temp)
|
await self.run(set_temperature, temperature=temp)
|
||||||
self.set(("climate_state_driver_temp_setting", temp))
|
self.set(("climate_state_driver_temp_setting", temp))
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ from homeassistant.helpers.update_coordinator import (
|
|||||||
CoordinatorEntity,
|
CoordinatorEntity,
|
||||||
DataUpdateCoordinator,
|
DataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
|
from homeassistant.util.dt import as_utc
|
||||||
|
|
||||||
from . import TileData
|
from . import TileData
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@ -145,15 +146,22 @@ class TileDeviceTracker(CoordinatorEntity[DataUpdateCoordinator[None]], TrackerE
|
|||||||
@callback
|
@callback
|
||||||
def _update_from_latest_data(self) -> None:
|
def _update_from_latest_data(self) -> None:
|
||||||
"""Update the entity from the latest data."""
|
"""Update the entity from the latest data."""
|
||||||
self._attr_extra_state_attributes.update(
|
self._attr_extra_state_attributes = {
|
||||||
{
|
|
||||||
ATTR_ALTITUDE: self._tile.altitude,
|
ATTR_ALTITUDE: self._tile.altitude,
|
||||||
ATTR_IS_LOST: self._tile.lost,
|
ATTR_IS_LOST: self._tile.lost,
|
||||||
ATTR_LAST_LOST_TIMESTAMP: self._tile.lost_timestamp,
|
|
||||||
ATTR_LAST_TIMESTAMP: self._tile.last_timestamp,
|
|
||||||
ATTR_RING_STATE: self._tile.ring_state,
|
ATTR_RING_STATE: self._tile.ring_state,
|
||||||
ATTR_VOIP_STATE: self._tile.voip_state,
|
ATTR_VOIP_STATE: self._tile.voip_state,
|
||||||
}
|
}
|
||||||
|
for timestamp_attr in (
|
||||||
|
(ATTR_LAST_LOST_TIMESTAMP, self._tile.lost_timestamp),
|
||||||
|
(ATTR_LAST_TIMESTAMP, self._tile.last_timestamp),
|
||||||
|
):
|
||||||
|
if not timestamp_attr[1]:
|
||||||
|
# If the API doesn't return a value for a particular timestamp
|
||||||
|
# attribute, skip it:
|
||||||
|
continue
|
||||||
|
self._attr_extra_state_attributes[timestamp_attr[0]] = as_utc(
|
||||||
|
timestamp_attr[1]
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
|
@ -78,7 +78,7 @@ class ProtectLight(ProtectDeviceEntity, LightEntity):
|
|||||||
is a change.
|
is a change.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return (self._attr_available, self._attr_brightness)
|
return (self._attr_available, self._attr_is_on, self._attr_brightness)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
|
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["holidays"],
|
"loggers": ["holidays"],
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["holidays==0.42"]
|
"requirements": ["holidays==0.43"]
|
||||||
}
|
}
|
||||||
|
@ -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 = 2
|
MINOR_VERSION: Final = 2
|
||||||
PATCH_VERSION: Final = "2"
|
PATCH_VERSION: Final = "3"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
||||||
|
@ -141,10 +141,6 @@ pubnub!=6.4.0
|
|||||||
# https://github.com/dahlia/iso4217/issues/16
|
# https://github.com/dahlia/iso4217/issues/16
|
||||||
iso4217!=1.10.20220401
|
iso4217!=1.10.20220401
|
||||||
|
|
||||||
# Matplotlib 3.6.2 has issues building wheels on armhf/armv7
|
|
||||||
# We need at least >=2.1.0 (tensorflow integration -> pycocotools)
|
|
||||||
matplotlib==3.6.1
|
|
||||||
|
|
||||||
# pyOpenSSL 24.0.0 or later required to avoid import errors when
|
# pyOpenSSL 24.0.0 or later required to avoid import errors when
|
||||||
# cryptography 42.0.0 is installed with botocore
|
# cryptography 42.0.0 is installed with botocore
|
||||||
pyOpenSSL>=24.0.0
|
pyOpenSSL>=24.0.0
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2024.2.2"
|
version = "2024.2.3"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
|
@ -191,7 +191,7 @@ aioairq==0.3.2
|
|||||||
aioairzone-cloud==0.3.8
|
aioairzone-cloud==0.3.8
|
||||||
|
|
||||||
# homeassistant.components.airzone
|
# homeassistant.components.airzone
|
||||||
aioairzone==0.7.2
|
aioairzone==0.7.4
|
||||||
|
|
||||||
# homeassistant.components.ambient_station
|
# homeassistant.components.ambient_station
|
||||||
aioambient==2024.01.0
|
aioambient==2024.01.0
|
||||||
@ -377,7 +377,7 @@ aioswitcher==3.4.1
|
|||||||
aiosyncthing==0.5.1
|
aiosyncthing==0.5.1
|
||||||
|
|
||||||
# homeassistant.components.tankerkoenig
|
# homeassistant.components.tankerkoenig
|
||||||
aiotankerkoenig==0.3.0
|
aiotankerkoenig==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.tractive
|
# homeassistant.components.tractive
|
||||||
aiotractive==0.5.6
|
aiotractive==0.5.6
|
||||||
@ -687,7 +687,7 @@ debugpy==1.8.0
|
|||||||
# decora==0.6
|
# decora==0.6
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
deebot-client==5.2.1
|
deebot-client==5.2.2
|
||||||
|
|
||||||
# homeassistant.components.ihc
|
# homeassistant.components.ihc
|
||||||
# homeassistant.components.namecheapdns
|
# homeassistant.components.namecheapdns
|
||||||
@ -695,7 +695,7 @@ deebot-client==5.2.1
|
|||||||
defusedxml==0.7.1
|
defusedxml==0.7.1
|
||||||
|
|
||||||
# homeassistant.components.deluge
|
# homeassistant.components.deluge
|
||||||
deluge-client==1.7.1
|
deluge-client==1.10.2
|
||||||
|
|
||||||
# homeassistant.components.lametric
|
# homeassistant.components.lametric
|
||||||
demetriek==0.4.0
|
demetriek==0.4.0
|
||||||
@ -967,7 +967,7 @@ gotailwind==0.2.2
|
|||||||
govee-ble==0.31.0
|
govee-ble==0.31.0
|
||||||
|
|
||||||
# homeassistant.components.govee_light_local
|
# homeassistant.components.govee_light_local
|
||||||
govee-local-api==1.4.1
|
govee-local-api==1.4.4
|
||||||
|
|
||||||
# homeassistant.components.remote_rpi_gpio
|
# homeassistant.components.remote_rpi_gpio
|
||||||
gpiozero==1.6.2
|
gpiozero==1.6.2
|
||||||
@ -1059,7 +1059,7 @@ hole==0.8.0
|
|||||||
|
|
||||||
# homeassistant.components.holiday
|
# homeassistant.components.holiday
|
||||||
# homeassistant.components.workday
|
# homeassistant.components.workday
|
||||||
holidays==0.42
|
holidays==0.43
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20240207.1
|
home-assistant-frontend==20240207.1
|
||||||
@ -1313,7 +1313,7 @@ moehlenhoff-alpha2==1.3.0
|
|||||||
mopeka-iot-ble==0.5.0
|
mopeka-iot-ble==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.motion_blinds
|
# homeassistant.components.motion_blinds
|
||||||
motionblinds==0.6.20
|
motionblinds==0.6.21
|
||||||
|
|
||||||
# homeassistant.components.motioneye
|
# homeassistant.components.motioneye
|
||||||
motioneye-client==0.3.14
|
motioneye-client==0.3.14
|
||||||
@ -2360,7 +2360,7 @@ pywaze==0.5.1
|
|||||||
pyweatherflowudp==1.4.5
|
pyweatherflowudp==1.4.5
|
||||||
|
|
||||||
# homeassistant.components.html5
|
# homeassistant.components.html5
|
||||||
pywebpush==1.9.2
|
pywebpush==1.14.1
|
||||||
|
|
||||||
# homeassistant.components.wemo
|
# homeassistant.components.wemo
|
||||||
pywemo==1.4.0
|
pywemo==1.4.0
|
||||||
@ -2423,7 +2423,7 @@ renault-api==0.2.1
|
|||||||
renson-endura-delta==1.7.1
|
renson-endura-delta==1.7.1
|
||||||
|
|
||||||
# homeassistant.components.reolink
|
# homeassistant.components.reolink
|
||||||
reolink-aio==0.8.7
|
reolink-aio==0.8.8
|
||||||
|
|
||||||
# homeassistant.components.idteck_prox
|
# homeassistant.components.idteck_prox
|
||||||
rfk101py==0.0.1
|
rfk101py==0.0.1
|
||||||
@ -2444,13 +2444,13 @@ rjpl==0.3.6
|
|||||||
rocketchat-API==0.6.1
|
rocketchat-API==0.6.1
|
||||||
|
|
||||||
# homeassistant.components.roku
|
# homeassistant.components.roku
|
||||||
rokuecp==0.19.0
|
rokuecp==0.19.1
|
||||||
|
|
||||||
# homeassistant.components.romy
|
# homeassistant.components.romy
|
||||||
romy==0.0.7
|
romy==0.0.7
|
||||||
|
|
||||||
# homeassistant.components.roomba
|
# homeassistant.components.roomba
|
||||||
roombapy==1.6.10
|
roombapy==1.6.12
|
||||||
|
|
||||||
# homeassistant.components.roon
|
# homeassistant.components.roon
|
||||||
roonapi==0.1.6
|
roonapi==0.1.6
|
||||||
|
@ -170,7 +170,7 @@ aioairq==0.3.2
|
|||||||
aioairzone-cloud==0.3.8
|
aioairzone-cloud==0.3.8
|
||||||
|
|
||||||
# homeassistant.components.airzone
|
# homeassistant.components.airzone
|
||||||
aioairzone==0.7.2
|
aioairzone==0.7.4
|
||||||
|
|
||||||
# homeassistant.components.ambient_station
|
# homeassistant.components.ambient_station
|
||||||
aioambient==2024.01.0
|
aioambient==2024.01.0
|
||||||
@ -350,7 +350,7 @@ aioswitcher==3.4.1
|
|||||||
aiosyncthing==0.5.1
|
aiosyncthing==0.5.1
|
||||||
|
|
||||||
# homeassistant.components.tankerkoenig
|
# homeassistant.components.tankerkoenig
|
||||||
aiotankerkoenig==0.3.0
|
aiotankerkoenig==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.tractive
|
# homeassistant.components.tractive
|
||||||
aiotractive==0.5.6
|
aiotractive==0.5.6
|
||||||
@ -562,7 +562,7 @@ dbus-fast==2.21.1
|
|||||||
debugpy==1.8.0
|
debugpy==1.8.0
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
deebot-client==5.2.1
|
deebot-client==5.2.2
|
||||||
|
|
||||||
# homeassistant.components.ihc
|
# homeassistant.components.ihc
|
||||||
# homeassistant.components.namecheapdns
|
# homeassistant.components.namecheapdns
|
||||||
@ -570,7 +570,7 @@ deebot-client==5.2.1
|
|||||||
defusedxml==0.7.1
|
defusedxml==0.7.1
|
||||||
|
|
||||||
# homeassistant.components.deluge
|
# homeassistant.components.deluge
|
||||||
deluge-client==1.7.1
|
deluge-client==1.10.2
|
||||||
|
|
||||||
# homeassistant.components.lametric
|
# homeassistant.components.lametric
|
||||||
demetriek==0.4.0
|
demetriek==0.4.0
|
||||||
@ -784,7 +784,7 @@ gotailwind==0.2.2
|
|||||||
govee-ble==0.31.0
|
govee-ble==0.31.0
|
||||||
|
|
||||||
# homeassistant.components.govee_light_local
|
# homeassistant.components.govee_light_local
|
||||||
govee-local-api==1.4.1
|
govee-local-api==1.4.4
|
||||||
|
|
||||||
# homeassistant.components.gpsd
|
# homeassistant.components.gpsd
|
||||||
gps3==0.33.3
|
gps3==0.33.3
|
||||||
@ -855,7 +855,7 @@ hole==0.8.0
|
|||||||
|
|
||||||
# homeassistant.components.holiday
|
# homeassistant.components.holiday
|
||||||
# homeassistant.components.workday
|
# homeassistant.components.workday
|
||||||
holidays==0.42
|
holidays==0.43
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20240207.1
|
home-assistant-frontend==20240207.1
|
||||||
@ -1049,7 +1049,7 @@ moehlenhoff-alpha2==1.3.0
|
|||||||
mopeka-iot-ble==0.5.0
|
mopeka-iot-ble==0.5.0
|
||||||
|
|
||||||
# homeassistant.components.motion_blinds
|
# homeassistant.components.motion_blinds
|
||||||
motionblinds==0.6.20
|
motionblinds==0.6.21
|
||||||
|
|
||||||
# homeassistant.components.motioneye
|
# homeassistant.components.motioneye
|
||||||
motioneye-client==0.3.14
|
motioneye-client==0.3.14
|
||||||
@ -1809,7 +1809,7 @@ pywaze==0.5.1
|
|||||||
pyweatherflowudp==1.4.5
|
pyweatherflowudp==1.4.5
|
||||||
|
|
||||||
# homeassistant.components.html5
|
# homeassistant.components.html5
|
||||||
pywebpush==1.9.2
|
pywebpush==1.14.1
|
||||||
|
|
||||||
# homeassistant.components.wemo
|
# homeassistant.components.wemo
|
||||||
pywemo==1.4.0
|
pywemo==1.4.0
|
||||||
@ -1857,7 +1857,7 @@ renault-api==0.2.1
|
|||||||
renson-endura-delta==1.7.1
|
renson-endura-delta==1.7.1
|
||||||
|
|
||||||
# homeassistant.components.reolink
|
# homeassistant.components.reolink
|
||||||
reolink-aio==0.8.7
|
reolink-aio==0.8.8
|
||||||
|
|
||||||
# homeassistant.components.rflink
|
# homeassistant.components.rflink
|
||||||
rflink==0.0.65
|
rflink==0.0.65
|
||||||
@ -1866,13 +1866,13 @@ rflink==0.0.65
|
|||||||
ring-doorbell[listen]==0.8.7
|
ring-doorbell[listen]==0.8.7
|
||||||
|
|
||||||
# homeassistant.components.roku
|
# homeassistant.components.roku
|
||||||
rokuecp==0.19.0
|
rokuecp==0.19.1
|
||||||
|
|
||||||
# homeassistant.components.romy
|
# homeassistant.components.romy
|
||||||
romy==0.0.7
|
romy==0.0.7
|
||||||
|
|
||||||
# homeassistant.components.roomba
|
# homeassistant.components.roomba
|
||||||
roombapy==1.6.10
|
roombapy==1.6.12
|
||||||
|
|
||||||
# homeassistant.components.roon
|
# homeassistant.components.roon
|
||||||
roonapi==0.1.6
|
roonapi==0.1.6
|
||||||
|
@ -134,10 +134,6 @@ pubnub!=6.4.0
|
|||||||
# https://github.com/dahlia/iso4217/issues/16
|
# https://github.com/dahlia/iso4217/issues/16
|
||||||
iso4217!=1.10.20220401
|
iso4217!=1.10.20220401
|
||||||
|
|
||||||
# Matplotlib 3.6.2 has issues building wheels on armhf/armv7
|
|
||||||
# We need at least >=2.1.0 (tensorflow integration -> pycocotools)
|
|
||||||
matplotlib==3.6.1
|
|
||||||
|
|
||||||
# pyOpenSSL 24.0.0 or later required to avoid import errors when
|
# pyOpenSSL 24.0.0 or later required to avoid import errors when
|
||||||
# cryptography 42.0.0 is installed with botocore
|
# cryptography 42.0.0 is installed with botocore
|
||||||
pyOpenSSL>=24.0.0
|
pyOpenSSL>=24.0.0
|
||||||
|
@ -118,7 +118,48 @@ async def test_apprise_notification(hass: HomeAssistant) -> None:
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# Validate calls were made under the hood correctly
|
# Validate calls were made under the hood correctly
|
||||||
obj.add.assert_called_once_with([config[BASE_COMPONENT]["url"]])
|
obj.add.assert_called_once_with(config[BASE_COMPONENT]["url"])
|
||||||
|
obj.notify.assert_called_once_with(
|
||||||
|
**{"body": data["message"], "title": data["title"], "tag": None}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_apprise_multiple_notification(hass: HomeAssistant) -> None:
|
||||||
|
"""Test apprise notification."""
|
||||||
|
|
||||||
|
config = {
|
||||||
|
BASE_COMPONENT: {
|
||||||
|
"name": "test",
|
||||||
|
"platform": "apprise",
|
||||||
|
"url": [
|
||||||
|
"mailto://user:pass@example.com, mailto://user:pass@gmail.com",
|
||||||
|
"json://user:pass@gmail.com",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Our Message
|
||||||
|
data = {"title": "Test Title", "message": "Test Message"}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.apprise.notify.apprise.Apprise"
|
||||||
|
) as mock_apprise:
|
||||||
|
obj = MagicMock()
|
||||||
|
obj.add.return_value = True
|
||||||
|
obj.notify.return_value = True
|
||||||
|
mock_apprise.return_value = obj
|
||||||
|
assert await async_setup_component(hass, BASE_COMPONENT, config)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Test the existence of our service
|
||||||
|
assert hass.services.has_service(BASE_COMPONENT, "test")
|
||||||
|
|
||||||
|
# Test the call to our underlining notify() call
|
||||||
|
await hass.services.async_call(BASE_COMPONENT, "test", data)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Validate 2 calls were made under the hood
|
||||||
|
assert obj.add.call_count == 2
|
||||||
obj.notify.assert_called_once_with(
|
obj.notify.assert_called_once_with(
|
||||||
**{"body": data["message"], "title": data["title"], "tag": None}
|
**{"body": data["message"], "title": data["title"], "tag": None}
|
||||||
)
|
)
|
||||||
|
@ -119,6 +119,25 @@ async def test_attribute(hass: HomeAssistant, service, attribute) -> None:
|
|||||||
assert calls_1[0].data == {"entity_id": ENTITY_1, attribute: value}
|
assert calls_1[0].data == {"entity_id": ENTITY_1, attribute: value}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("service", "attribute"),
|
||||||
|
[
|
||||||
|
(SERVICE_SET_PRESET_MODE, ATTR_PRESET_MODE),
|
||||||
|
(SERVICE_SET_SWING_MODE, ATTR_SWING_MODE),
|
||||||
|
(SERVICE_SET_FAN_MODE, ATTR_FAN_MODE),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_attribute_with_none(hass: HomeAssistant, service, attribute) -> None:
|
||||||
|
"""Test that service call is not made for attributes with None value."""
|
||||||
|
calls_1 = async_mock_service(hass, DOMAIN, service)
|
||||||
|
|
||||||
|
await async_reproduce_states(hass, [State(ENTITY_1, None, {attribute: None})])
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(calls_1) == 0
|
||||||
|
|
||||||
|
|
||||||
async def test_attribute_partial_temperature(hass: HomeAssistant) -> None:
|
async def test_attribute_partial_temperature(hass: HomeAssistant) -> None:
|
||||||
"""Test that service call ignores null attributes."""
|
"""Test that service call ignores null attributes."""
|
||||||
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_SET_TEMPERATURE)
|
calls_1 = async_mock_service(hass, DOMAIN, SERVICE_SET_TEMPERATURE)
|
||||||
|
@ -5,6 +5,7 @@ from unittest.mock import Mock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.cloud import CloudNotAvailable
|
||||||
from homeassistant.components.mobile_app.const import (
|
from homeassistant.components.mobile_app.const import (
|
||||||
ATTR_DEVICE_NAME,
|
ATTR_DEVICE_NAME,
|
||||||
CONF_CLOUDHOOK_URL,
|
CONF_CLOUDHOOK_URL,
|
||||||
@ -118,6 +119,32 @@ async def test_create_cloud_hook_on_setup(
|
|||||||
await _test_create_cloud_hook(hass, hass_admin_user, {}, True, additional_steps)
|
await _test_create_cloud_hook(hass, hass_admin_user, {}, True, additional_steps)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("exception", (CloudNotAvailable, ValueError))
|
||||||
|
async def test_remove_cloudhook(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_admin_user: MockUser,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
exception: Exception,
|
||||||
|
) -> None:
|
||||||
|
"""Test removing a cloud hook when config entry is removed."""
|
||||||
|
|
||||||
|
async def additional_steps(
|
||||||
|
config_entry: ConfigEntry, mock_create_cloudhook: Mock, cloud_hook: str
|
||||||
|
) -> None:
|
||||||
|
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
|
||||||
|
assert config_entry.data[CONF_CLOUDHOOK_URL] == cloud_hook
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.cloud.async_delete_cloudhook",
|
||||||
|
side_effect=exception,
|
||||||
|
) as delete_cloudhook:
|
||||||
|
await hass.config_entries.async_remove(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
delete_cloudhook.assert_called_once_with(hass, webhook_id)
|
||||||
|
assert str(exception) not in caplog.text
|
||||||
|
|
||||||
|
await _test_create_cloud_hook(hass, hass_admin_user, {}, True, additional_steps)
|
||||||
|
|
||||||
|
|
||||||
async def test_create_cloud_hook_aleady_exists(
|
async def test_create_cloud_hook_aleady_exists(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_admin_user: MockUser,
|
hass_admin_user: MockUser,
|
||||||
|
@ -98,12 +98,12 @@ def _mocked_discovery(*_):
|
|||||||
|
|
||||||
roomba = RoombaInfo(
|
roomba = RoombaInfo(
|
||||||
hostname="irobot-BLID",
|
hostname="irobot-BLID",
|
||||||
robot_name="robot_name",
|
robotname="robot_name",
|
||||||
ip=MOCK_IP,
|
ip=MOCK_IP,
|
||||||
mac="mac",
|
mac="mac",
|
||||||
firmware="firmware",
|
sw="firmware",
|
||||||
sku="sku",
|
sku="sku",
|
||||||
capabilities="capabilities",
|
cap={"cap": 1},
|
||||||
)
|
)
|
||||||
|
|
||||||
roomba_discovery.get_all = MagicMock(return_value=[roomba])
|
roomba_discovery.get_all = MagicMock(return_value=[roomba])
|
||||||
|
51
tests/components/tankerkoenig/test_coordinator.py
Normal file
51
tests/components/tankerkoenig/test_coordinator.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"""Tests for the Tankerkoening integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from aiotankerkoenig.exceptions import TankerkoenigRateLimitError
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.tankerkoenig.const import DEFAULT_SCAN_INTERVAL
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import STATE_UNAVAILABLE
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("setup_integration")
|
||||||
|
async def test_rate_limit(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
tankerkoenig: AsyncMock,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test detection of API rate limit."""
|
||||||
|
assert config_entry.state == ConfigEntryState.LOADED
|
||||||
|
state = hass.states.get("binary_sensor.station_somewhere_street_1_status")
|
||||||
|
assert state
|
||||||
|
assert state.state == "on"
|
||||||
|
|
||||||
|
tankerkoenig.prices.side_effect = TankerkoenigRateLimitError
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, dt_util.utcnow() + timedelta(minutes=DEFAULT_SCAN_INTERVAL)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert (
|
||||||
|
"API rate limit reached, consider to increase polling interval" in caplog.text
|
||||||
|
)
|
||||||
|
state = hass.states.get("binary_sensor.station_somewhere_street_1_status")
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
tankerkoenig.prices.side_effect = None
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, dt_util.utcnow() + timedelta(minutes=DEFAULT_SCAN_INTERVAL * 2)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
state = hass.states.get("binary_sensor.station_somewhere_street_1_status")
|
||||||
|
assert state
|
||||||
|
assert state.state == "on"
|
@ -37,6 +37,16 @@ def mock_wake_up():
|
|||||||
yield mock_wake_up
|
yield mock_wake_up
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_vehicle():
|
||||||
|
"""Mock Tesla Fleet API Vehicle Specific vehicle method."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.teslemetry.VehicleSpecific.vehicle",
|
||||||
|
return_value=WAKE_UP_ONLINE,
|
||||||
|
) as mock_vehicle:
|
||||||
|
yield mock_vehicle
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def mock_request():
|
def mock_request():
|
||||||
"""Mock Tesla Fleet API Vehicle Specific class."""
|
"""Mock Tesla Fleet API Vehicle Specific class."""
|
||||||
|
@ -26,6 +26,7 @@ from homeassistant.exceptions import HomeAssistantError
|
|||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from . import assert_entities, setup_platform
|
from . import assert_entities, setup_platform
|
||||||
|
from .const import WAKE_UP_ASLEEP, WAKE_UP_ONLINE
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
from tests.common import async_fire_time_changed
|
||||||
|
|
||||||
@ -108,7 +109,11 @@ async def test_errors(
|
|||||||
|
|
||||||
|
|
||||||
async def test_asleep_or_offline(
|
async def test_asleep_or_offline(
|
||||||
hass: HomeAssistant, mock_vehicle_data, freezer: FrozenDateTimeFactory
|
hass: HomeAssistant,
|
||||||
|
mock_vehicle_data,
|
||||||
|
mock_wake_up,
|
||||||
|
mock_vehicle,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Tests asleep is handled."""
|
"""Tests asleep is handled."""
|
||||||
|
|
||||||
@ -123,9 +128,47 @@ async def test_asleep_or_offline(
|
|||||||
async_fire_time_changed(hass)
|
async_fire_time_changed(hass)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
mock_vehicle_data.assert_called_once()
|
mock_vehicle_data.assert_called_once()
|
||||||
|
mock_wake_up.reset_mock()
|
||||||
|
|
||||||
# Run a command that will wake up the vehicle, but not immediately
|
# Run a command but fail trying to wake up the vehicle
|
||||||
|
mock_wake_up.side_effect = InvalidCommand
|
||||||
|
with pytest.raises(HomeAssistantError) as error:
|
||||||
|
await hass.services.async_call(
|
||||||
|
CLIMATE_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: [entity_id]},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert error
|
||||||
|
mock_wake_up.assert_called_once()
|
||||||
|
|
||||||
|
mock_wake_up.side_effect = None
|
||||||
|
mock_wake_up.reset_mock()
|
||||||
|
|
||||||
|
# Run a command but timeout trying to wake up the vehicle
|
||||||
|
mock_wake_up.return_value = WAKE_UP_ASLEEP
|
||||||
|
mock_vehicle.return_value = WAKE_UP_ASLEEP
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.teslemetry.entity.asyncio.sleep"
|
||||||
|
), pytest.raises(HomeAssistantError) as error:
|
||||||
|
await hass.services.async_call(
|
||||||
|
CLIMATE_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: [entity_id]},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert error
|
||||||
|
mock_wake_up.assert_called_once()
|
||||||
|
mock_vehicle.assert_called()
|
||||||
|
|
||||||
|
mock_wake_up.reset_mock()
|
||||||
|
mock_vehicle.reset_mock()
|
||||||
|
mock_wake_up.return_value = WAKE_UP_ONLINE
|
||||||
|
mock_vehicle.return_value = WAKE_UP_ONLINE
|
||||||
|
|
||||||
|
# Run a command and wake up the vehicle immediately
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
CLIMATE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: [entity_id]}, blocking=True
|
CLIMATE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: [entity_id]}, blocking=True
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
mock_wake_up.assert_called_once()
|
||||||
|
@ -54,14 +54,22 @@ async def test_climate(
|
|||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.tessie.climate.set_temperature",
|
"homeassistant.components.tessie.climate.set_temperature",
|
||||||
return_value=TEST_RESPONSE,
|
return_value=TEST_RESPONSE,
|
||||||
) as mock_set:
|
) as mock_set, patch(
|
||||||
|
"homeassistant.components.tessie.climate.start_climate_preconditioning",
|
||||||
|
return_value=TEST_RESPONSE,
|
||||||
|
) as mock_set2:
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
CLIMATE_DOMAIN,
|
CLIMATE_DOMAIN,
|
||||||
SERVICE_SET_TEMPERATURE,
|
SERVICE_SET_TEMPERATURE,
|
||||||
{ATTR_ENTITY_ID: [entity_id], ATTR_TEMPERATURE: 20},
|
{
|
||||||
|
ATTR_ENTITY_ID: [entity_id],
|
||||||
|
ATTR_HVAC_MODE: HVACMode.HEAT_COOL,
|
||||||
|
ATTR_TEMPERATURE: 20,
|
||||||
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
mock_set.assert_called_once()
|
mock_set.assert_called_once()
|
||||||
|
mock_set2.assert_called_once()
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
assert state.attributes[ATTR_TEMPERATURE] == 20
|
assert state.attributes[ATTR_TEMPERATURE] == 20
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user