mirror of
https://github.com/home-assistant/core.git
synced 2026-02-07 15:46:19 +01:00
Compare commits
94 Commits
fix-host-d
...
2023.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b8d4235c3 | ||
|
|
4ce859b4e4 | ||
|
|
18acec32b8 | ||
|
|
cfa2f2ce61 | ||
|
|
aa5ea5ebc3 | ||
|
|
bcea021c14 | ||
|
|
ea2d2ba7b7 | ||
|
|
c5f21fefbe | ||
|
|
9910f9e0ae | ||
|
|
f0a06efa1f | ||
|
|
8992d15ffc | ||
|
|
e097dc02dd | ||
|
|
bfae1468d6 | ||
|
|
09ed6e9f9b | ||
|
|
040ecb74e0 | ||
|
|
a48e63aa28 | ||
|
|
19479b2a68 | ||
|
|
9ae29e243d | ||
|
|
e309bd764b | ||
|
|
777ffe6946 | ||
|
|
fa0f679a9a | ||
|
|
26b7e94c4f | ||
|
|
957998ea8d | ||
|
|
abaeacbd6b | ||
|
|
d76c16fa3a | ||
|
|
67edb98e59 | ||
|
|
376a79eb42 | ||
|
|
41500cbe9b | ||
|
|
06f27e7e74 | ||
|
|
a3ebfaebe7 | ||
|
|
8d781ff063 | ||
|
|
bac39f0061 | ||
|
|
c7b702f3c2 | ||
|
|
3728f3da69 | ||
|
|
31d8f4b35d | ||
|
|
f113d9aa71 | ||
|
|
891ad0b1be | ||
|
|
5c16a8247a | ||
|
|
483671bf9f | ||
|
|
6f73d2aac5 | ||
|
|
f5b3661836 | ||
|
|
f70c13214c | ||
|
|
70e8978123 | ||
|
|
031b1c26ce | ||
|
|
13580a334f | ||
|
|
e81bfb959e | ||
|
|
fefe930506 | ||
|
|
5ac7e8b1ac | ||
|
|
36512f7157 | ||
|
|
cc3ae9e103 | ||
|
|
12482216f6 | ||
|
|
20409d0124 | ||
|
|
a741bc9951 | ||
|
|
59d2bce369 | ||
|
|
eef318f63c | ||
|
|
9c8a4bb4eb | ||
|
|
9c9f1ea685 | ||
|
|
85d999b020 | ||
|
|
bcddf52364 | ||
|
|
07e4e1379a | ||
|
|
f9f010643a | ||
|
|
974c34e2b6 | ||
|
|
1c3de76b04 | ||
|
|
bee63ca654 | ||
|
|
29c99f419f | ||
|
|
3d321c5ca7 | ||
|
|
4617c16a96 | ||
|
|
a60656bf29 | ||
|
|
2eb2a65197 | ||
|
|
867aaf10ee | ||
|
|
7fe1ac901f | ||
|
|
5dca3844ef | ||
|
|
b5c75a2f2f | ||
|
|
62fc9dfd6c | ||
|
|
0573981d6f | ||
|
|
cc7a4d01e3 | ||
|
|
293025ab6c | ||
|
|
a490b5e286 | ||
|
|
7e4da1d03b | ||
|
|
9e140864eb | ||
|
|
a6f88fb123 | ||
|
|
386c5ecc3e | ||
|
|
0d7fb5b026 | ||
|
|
767b7ba4d6 | ||
|
|
f2cef7245a | ||
|
|
701a5d7758 | ||
|
|
244fccdae6 | ||
|
|
10e6a26717 | ||
|
|
5fe5013198 | ||
|
|
0a0584b053 | ||
|
|
62733e830f | ||
|
|
bbcfb5f30e | ||
|
|
5b0e0b07b3 | ||
|
|
05fd64fe80 |
@@ -45,6 +45,7 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/switch/**
|
||||
- homeassistant/components/text/**
|
||||
- homeassistant/components/time/**
|
||||
- homeassistant/components/todo/**
|
||||
- homeassistant/components/tts/**
|
||||
- homeassistant/components/update/**
|
||||
- homeassistant/components/vacuum/**
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED
|
||||
|
||||
# These are events that do not contain any sensitive data
|
||||
# Except for state_changed, which is handled accordingly.
|
||||
@@ -28,6 +29,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[str]] = {
|
||||
EVENT_CORE_CONFIG_UPDATE,
|
||||
EVENT_DEVICE_REGISTRY_UPDATED,
|
||||
EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED,
|
||||
EVENT_LOVELACE_UPDATED,
|
||||
EVENT_PANELS_UPDATED,
|
||||
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.3.0"]
|
||||
"requirements": ["aioairzone-cloud==0.3.1"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"loggers": ["adb_shell", "androidtv", "pure_python_adb"],
|
||||
"requirements": [
|
||||
"adb-shell[async]==0.4.4",
|
||||
"androidtv[async]==0.0.72",
|
||||
"androidtv[async]==0.0.73",
|
||||
"pure-python-adb[async]==0.3.0.dev0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,6 +21,15 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
COMMAND_TO_ATTRIBUTE = {
|
||||
"wakeup": ("power", "turn_on"),
|
||||
"suspend": ("power", "turn_off"),
|
||||
"turn_on": ("power", "turn_on"),
|
||||
"turn_off": ("power", "turn_off"),
|
||||
"volume_up": ("audio", "volume_up"),
|
||||
"volume_down": ("audio", "volume_down"),
|
||||
"home_hold": ("remote_control", "home"),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -61,7 +70,13 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity):
|
||||
|
||||
for _ in range(num_repeats):
|
||||
for single_command in command:
|
||||
attr_value = getattr(self.atv.remote_control, single_command, None)
|
||||
attr_value = None
|
||||
if attributes := COMMAND_TO_ATTRIBUTE.get(single_command):
|
||||
attr_value = self.atv
|
||||
for attr_name in attributes:
|
||||
attr_value = getattr(attr_value, attr_name, None)
|
||||
if not attr_value:
|
||||
attr_value = getattr(self.atv.remote_control, single_command, None)
|
||||
if not attr_value:
|
||||
raise ValueError("Command not found. Exiting sequence")
|
||||
|
||||
|
||||
@@ -86,8 +86,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
blink.auth = Auth(auth_data, no_prompt=True, session=session)
|
||||
blink.refresh_rate = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
coordinator = BlinkUpdateCoordinator(hass, blink)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
try:
|
||||
await blink.start()
|
||||
@@ -101,6 +99,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if not blink.available:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
|
||||
@@ -330,7 +330,7 @@ class BaseHaRemoteScanner(BaseHaScanner):
|
||||
prev_manufacturer_data = prev_advertisement.manufacturer_data
|
||||
prev_name = prev_device.name
|
||||
|
||||
if local_name and prev_name and len(prev_name) > len(local_name):
|
||||
if prev_name and (not local_name or len(prev_name) > len(local_name)):
|
||||
local_name = prev_name
|
||||
|
||||
if service_uuids and service_uuids != prev_service_uuids:
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==0.21.1",
|
||||
"bleak-retry-connector==3.2.1",
|
||||
"bleak-retry-connector==3.3.0",
|
||||
"bluetooth-adapters==0.16.1",
|
||||
"bluetooth-auto-recovery==1.2.3",
|
||||
"bluetooth-data-tools==1.13.0",
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Generic, TypedDict, TypeVar, cast
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
ATTR_CONNECTIONS,
|
||||
ATTR_IDENTIFIERS,
|
||||
ATTR_NAME,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
@@ -16,7 +17,7 @@ from homeassistant.const import (
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_platform import async_get_current_platform
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
@@ -644,6 +645,8 @@ class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProce
|
||||
self._attr_unique_id = f"{address}-{key}"
|
||||
if ATTR_NAME not in self._attr_device_info:
|
||||
self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name
|
||||
if device_id is None:
|
||||
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_BLUETOOTH, address)}
|
||||
self._attr_name = processor.entity_names.get(entity_key)
|
||||
|
||||
@property
|
||||
|
||||
@@ -5,9 +5,9 @@ from typing import cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, Platform
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import DeviceAutomationType, async_get_device_automation_platform
|
||||
@@ -55,31 +55,42 @@ async def async_validate_device_automation_config(
|
||||
platform = await async_get_device_automation_platform(
|
||||
hass, validated_config[CONF_DOMAIN], automation_type
|
||||
)
|
||||
|
||||
# Make sure the referenced device and optional entity exist
|
||||
device_registry = dr.async_get(hass)
|
||||
if not (device := device_registry.async_get(validated_config[CONF_DEVICE_ID])):
|
||||
# The device referenced by the device automation does not exist
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Unknown device '{validated_config[CONF_DEVICE_ID]}'"
|
||||
)
|
||||
if entity_id := validated_config.get(CONF_ENTITY_ID):
|
||||
try:
|
||||
er.async_validate_entity_id(er.async_get(hass), entity_id)
|
||||
except vol.Invalid as err:
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Unknown entity '{entity_id}'"
|
||||
) from err
|
||||
|
||||
if not hasattr(platform, DYNAMIC_VALIDATOR[automation_type]):
|
||||
# Pass the unvalidated config to avoid mutating the raw config twice
|
||||
return cast(
|
||||
ConfigType, getattr(platform, STATIC_VALIDATOR[automation_type])(config)
|
||||
)
|
||||
|
||||
# Bypass checks for entity platforms
|
||||
# Devices are not linked to config entries from entity platform domains, skip
|
||||
# the checks below which look for a config entry matching the device automation
|
||||
# domain
|
||||
if (
|
||||
automation_type == DeviceAutomationType.ACTION
|
||||
and validated_config[CONF_DOMAIN] in ENTITY_PLATFORMS
|
||||
):
|
||||
# Pass the unvalidated config to avoid mutating the raw config twice
|
||||
return cast(
|
||||
ConfigType,
|
||||
await getattr(platform, DYNAMIC_VALIDATOR[automation_type])(hass, config),
|
||||
)
|
||||
|
||||
# Only call the dynamic validator if the referenced device exists and the relevant
|
||||
# config entry is loaded
|
||||
registry = dr.async_get(hass)
|
||||
if not (device := registry.async_get(validated_config[CONF_DEVICE_ID])):
|
||||
# The device referenced by the device automation does not exist
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Unknown device '{validated_config[CONF_DEVICE_ID]}'"
|
||||
)
|
||||
|
||||
# Find a config entry with the same domain as the device automation
|
||||
device_config_entry = None
|
||||
for entry_id in device.config_entries:
|
||||
if (
|
||||
@@ -91,7 +102,7 @@ async def async_validate_device_automation_config(
|
||||
break
|
||||
|
||||
if not device_config_entry:
|
||||
# The config entry referenced by the device automation does not exist
|
||||
# There's no config entry with the same domain as the device automation
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Device '{validated_config[CONF_DEVICE_ID]}' has no config entry from "
|
||||
f"domain '{validated_config[CONF_DOMAIN]}'"
|
||||
|
||||
@@ -141,6 +141,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
translation_key="gas_meter_usage",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:fire",
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
@@ -283,6 +284,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
key="dsmr/day-consumption/gas",
|
||||
translation_key="daily_gas_usage",
|
||||
icon="mdi:counter",
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
@@ -460,6 +462,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
key="dsmr/current-month/gas",
|
||||
translation_key="current_month_gas_usage",
|
||||
icon="mdi:counter",
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
@@ -538,6 +541,7 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
key="dsmr/current-year/gas",
|
||||
translation_key="current_year_gas_usage",
|
||||
icon="mdi:counter",
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/econet",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["paho_mqtt", "pyeconet"],
|
||||
"requirements": ["pyeconet==0.1.20"]
|
||||
"requirements": ["pyeconet==0.1.22"]
|
||||
}
|
||||
|
||||
@@ -487,6 +487,18 @@ class EvoBroker:
|
||||
)
|
||||
self.temps = None # these are now stale, will fall back to v2 temps
|
||||
|
||||
except KeyError as err:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Unable to obtain high-precision temperatures. "
|
||||
"It appears the JSON schema is not as expected, "
|
||||
"so the high-precision feature will be disabled until next restart."
|
||||
"Message is: %s"
|
||||
),
|
||||
err,
|
||||
)
|
||||
self.client_v1 = self.temps = None
|
||||
|
||||
else:
|
||||
if (
|
||||
str(self.client_v1.location_id)
|
||||
@@ -495,7 +507,7 @@ class EvoBroker:
|
||||
_LOGGER.warning(
|
||||
"The v2 API's configured location doesn't match "
|
||||
"the v1 API's default location (there is more than one location), "
|
||||
"so the high-precision feature will be disabled"
|
||||
"so the high-precision feature will be disabled until next restart"
|
||||
)
|
||||
self.client_v1 = self.temps = None
|
||||
else:
|
||||
|
||||
@@ -3,14 +3,24 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_automation import toggle_entity
|
||||
from homeassistant.components.device_automation import (
|
||||
async_validate_entity_schema,
|
||||
toggle_entity,
|
||||
)
|
||||
from homeassistant.const import CONF_DOMAIN
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN})
|
||||
_ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN})
|
||||
|
||||
|
||||
async def async_validate_action_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return async_validate_entity_schema(hass, config, _ACTION_SCHEMA)
|
||||
|
||||
|
||||
async def async_get_actions(
|
||||
|
||||
@@ -59,13 +59,16 @@ class FitbitOAuth2Implementation(AuthImplementation):
|
||||
resp = await session.post(self.token_url, data=data, headers=self._headers)
|
||||
resp.raise_for_status()
|
||||
except aiohttp.ClientResponseError as err:
|
||||
error_body = await resp.text()
|
||||
_LOGGER.debug("Client response error body: %s", error_body)
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
error_body = await resp.text() if not session.closed else ""
|
||||
_LOGGER.debug(
|
||||
"Client response error status=%s, body=%s", err.status, error_body
|
||||
)
|
||||
if err.status == HTTPStatus.UNAUTHORIZED:
|
||||
raise FitbitAuthException from err
|
||||
raise FitbitApiException from err
|
||||
raise FitbitAuthException(f"Unauthorized error: {err}") from err
|
||||
raise FitbitApiException(f"Server error response: {err}") from err
|
||||
except aiohttp.ClientError as err:
|
||||
raise FitbitApiException from err
|
||||
raise FitbitApiException(f"Client connection error: {err}") from err
|
||||
return cast(dict, await resp.json())
|
||||
|
||||
@property
|
||||
|
||||
@@ -53,6 +53,21 @@ class OAuth2FlowHandler(
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_creation(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Create config entry from external data with Fitbit specific error handling."""
|
||||
try:
|
||||
return await super().async_step_creation()
|
||||
except FitbitAuthException as err:
|
||||
_LOGGER.error(
|
||||
"Failed to authenticate when creating Fitbit credentials: %s", err
|
||||
)
|
||||
return self.async_abort(reason="invalid_auth")
|
||||
except FitbitApiException as err:
|
||||
_LOGGER.error("Failed to create Fitbit credentials: %s", err)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
|
||||
"""Create an entry for the flow, or update existing entry."""
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import logging
|
||||
import os
|
||||
from typing import Any, Final, cast
|
||||
|
||||
from fitbit import Fitbit
|
||||
from oauthlib.oauth2.rfc6749.errors import OAuth2Error
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.application_credentials import (
|
||||
@@ -567,34 +569,54 @@ async def async_setup_platform(
|
||||
|
||||
if config_file is not None:
|
||||
_LOGGER.debug("Importing existing fitbit.conf application credentials")
|
||||
await async_import_client_credential(
|
||||
hass,
|
||||
DOMAIN,
|
||||
ClientCredential(
|
||||
config_file[CONF_CLIENT_ID], config_file[CONF_CLIENT_SECRET]
|
||||
),
|
||||
|
||||
# Refresh the token before importing to ensure it is working and not
|
||||
# expired on first initialization.
|
||||
authd_client = Fitbit(
|
||||
config_file[CONF_CLIENT_ID],
|
||||
config_file[CONF_CLIENT_SECRET],
|
||||
access_token=config_file[ATTR_ACCESS_TOKEN],
|
||||
refresh_token=config_file[ATTR_REFRESH_TOKEN],
|
||||
expires_at=config_file[ATTR_LAST_SAVED_AT],
|
||||
refresh_cb=lambda x: None,
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
"auth_implementation": DOMAIN,
|
||||
CONF_TOKEN: {
|
||||
ATTR_ACCESS_TOKEN: config_file[ATTR_ACCESS_TOKEN],
|
||||
ATTR_REFRESH_TOKEN: config_file[ATTR_REFRESH_TOKEN],
|
||||
"expires_at": config_file[ATTR_LAST_SAVED_AT],
|
||||
},
|
||||
CONF_CLOCK_FORMAT: config[CONF_CLOCK_FORMAT],
|
||||
CONF_UNIT_SYSTEM: config[CONF_UNIT_SYSTEM],
|
||||
CONF_MONITORED_RESOURCES: config[CONF_MONITORED_RESOURCES],
|
||||
},
|
||||
)
|
||||
translation_key = "deprecated_yaml_import"
|
||||
if (
|
||||
result.get("type") == FlowResultType.ABORT
|
||||
and result.get("reason") == "cannot_connect"
|
||||
):
|
||||
try:
|
||||
updated_token = await hass.async_add_executor_job(
|
||||
authd_client.client.refresh_token
|
||||
)
|
||||
except OAuth2Error as err:
|
||||
_LOGGER.debug("Unable to import fitbit OAuth2 credentials: %s", err)
|
||||
translation_key = "deprecated_yaml_import_issue_cannot_connect"
|
||||
else:
|
||||
await async_import_client_credential(
|
||||
hass,
|
||||
DOMAIN,
|
||||
ClientCredential(
|
||||
config_file[CONF_CLIENT_ID], config_file[CONF_CLIENT_SECRET]
|
||||
),
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
"auth_implementation": DOMAIN,
|
||||
CONF_TOKEN: {
|
||||
ATTR_ACCESS_TOKEN: updated_token[ATTR_ACCESS_TOKEN],
|
||||
ATTR_REFRESH_TOKEN: updated_token[ATTR_REFRESH_TOKEN],
|
||||
"expires_at": updated_token["expires_at"],
|
||||
"scope": " ".join(updated_token.get("scope", [])),
|
||||
},
|
||||
CONF_CLOCK_FORMAT: config[CONF_CLOCK_FORMAT],
|
||||
CONF_UNIT_SYSTEM: config[CONF_UNIT_SYSTEM],
|
||||
CONF_MONITORED_RESOURCES: config[CONF_MONITORED_RESOURCES],
|
||||
},
|
||||
)
|
||||
translation_key = "deprecated_yaml_import"
|
||||
if (
|
||||
result.get("type") == FlowResultType.ABORT
|
||||
and result.get("reason") == "cannot_connect"
|
||||
):
|
||||
translation_key = "deprecated_yaml_import_issue_cannot_connect"
|
||||
else:
|
||||
translation_key = "deprecated_yaml_no_import"
|
||||
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"wrong_account": "The user credentials provided do not match this Fitbit account."
|
||||
|
||||
@@ -388,6 +388,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
# Can be removed in 2023
|
||||
hass.http.register_redirect("/config/server_control", "/developer-tools/yaml")
|
||||
|
||||
# Shopping list panel was replaced by todo panel in 2023.11
|
||||
hass.http.register_redirect("/shopping-list", "/todo")
|
||||
|
||||
hass.http.app.router.register_resource(IndexView(repo_path, hass))
|
||||
|
||||
async_register_built_in_panel(hass, "profile")
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20231025.1"]
|
||||
"requirements": ["home-assistant-frontend==20231030.1"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/geniushub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["geniushubclient"],
|
||||
"requirements": ["geniushub-client==0.7.0"]
|
||||
"requirements": ["geniushub-client==0.7.1"]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"""Support for Google Mail."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aiohttp.client_exceptions import ClientError, ClientResponseError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session,
|
||||
@@ -35,16 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
auth = AsyncConfigEntryAuth(session)
|
||||
try:
|
||||
await auth.check_and_refresh_token()
|
||||
except ClientResponseError as err:
|
||||
if 400 <= err.status < 500:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"OAuth session is not valid, reauth required"
|
||||
) from err
|
||||
raise ConfigEntryNotReady from err
|
||||
except ClientError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
await auth.check_and_refresh_token()
|
||||
hass.data[DOMAIN][entry.entry_id] = auth
|
||||
|
||||
hass.async_create_task(
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
"""API for Google Mail bound to Home Assistant OAuth."""
|
||||
from aiohttp.client_exceptions import ClientError, ClientResponseError
|
||||
from google.auth.exceptions import RefreshError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import Resource, build
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
|
||||
@@ -24,14 +31,30 @@ class AsyncConfigEntryAuth:
|
||||
|
||||
async def check_and_refresh_token(self) -> str:
|
||||
"""Check the token."""
|
||||
await self.oauth_session.async_ensure_token_valid()
|
||||
try:
|
||||
await self.oauth_session.async_ensure_token_valid()
|
||||
except (RefreshError, ClientResponseError, ClientError) as ex:
|
||||
if (
|
||||
self.oauth_session.config_entry.state
|
||||
is ConfigEntryState.SETUP_IN_PROGRESS
|
||||
):
|
||||
if isinstance(ex, ClientResponseError) and 400 <= ex.status < 500:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"OAuth session is not valid, reauth required"
|
||||
) from ex
|
||||
raise ConfigEntryNotReady from ex
|
||||
if (
|
||||
isinstance(ex, RefreshError)
|
||||
or hasattr(ex, "status")
|
||||
and ex.status == 400
|
||||
):
|
||||
self.oauth_session.config_entry.async_start_reauth(
|
||||
self.oauth_session.hass
|
||||
)
|
||||
raise HomeAssistantError(ex) from ex
|
||||
return self.access_token
|
||||
|
||||
async def get_resource(self) -> Resource:
|
||||
"""Get current resource."""
|
||||
try:
|
||||
credentials = Credentials(await self.check_and_refresh_token())
|
||||
except RefreshError as ex:
|
||||
self.oauth_session.config_entry.async_start_reauth(self.oauth_session.hass)
|
||||
raise ex
|
||||
credentials = Credentials(await self.check_and_refresh_token())
|
||||
return build("gmail", "v1", credentials=credentials)
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import build
|
||||
from googleapiclient.errors import HttpError
|
||||
from googleapiclient.http import HttpRequest
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .const import DOMAIN, OAUTH2_SCOPES
|
||||
@@ -28,3 +35,24 @@ class OAuth2FlowHandler(
|
||||
"access_type": "offline",
|
||||
"prompt": "consent",
|
||||
}
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
|
||||
"""Create an entry for the flow."""
|
||||
try:
|
||||
resource = build(
|
||||
"tasks",
|
||||
"v1",
|
||||
credentials=Credentials(token=data[CONF_TOKEN][CONF_ACCESS_TOKEN]),
|
||||
)
|
||||
cmd: HttpRequest = resource.tasklists().list()
|
||||
await self.hass.async_add_executor_job(cmd.execute)
|
||||
except HttpError as ex:
|
||||
error = ex.reason
|
||||
return self.async_abort(
|
||||
reason="access_not_configured",
|
||||
description_placeholders={"message": error},
|
||||
)
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
self.logger.exception("Unknown error occurred: %s", ex)
|
||||
return self.async_abort(reason="unknown")
|
||||
return self.async_create_entry(title=self.flow_impl.name, data=data)
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]"
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
||||
"access_not_configured": "Unable to access the Google API:\n\n{message}",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhap"],
|
||||
"requirements": [
|
||||
"HAP-python==4.9.0",
|
||||
"HAP-python==4.9.1",
|
||||
"fnv-hash-fast==0.5.0",
|
||||
"PyQRCode==1.2.1",
|
||||
"base36==0.1.1"
|
||||
|
||||
@@ -884,7 +884,9 @@ class HKDevice:
|
||||
self._config_changed_callbacks.add(callback_)
|
||||
return partial(self._remove_config_changed_callback, callback_)
|
||||
|
||||
async def get_characteristics(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
||||
async def get_characteristics(
|
||||
self, *args: Any, **kwargs: Any
|
||||
) -> dict[tuple[int, int], dict[str, Any]]:
|
||||
"""Read latest state from homekit accessory."""
|
||||
return await self.pairing.get_characteristics(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.0.8"],
|
||||
"requirements": ["aiohomekit==3.0.9"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ async def async_setup_entry(
|
||||
class HomeWizardIdentifyButton(HomeWizardEntity, ButtonEntity):
|
||||
"""Representation of a identify button."""
|
||||
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_device_class = ButtonDeviceClass.IDENTIFY
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -62,7 +62,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
self._abort_if_unique_id_configured(updates=user_input)
|
||||
return self.async_create_entry(
|
||||
title=f"{device_info.product_name} ({device_info.serial})",
|
||||
title=f"{device_info.product_name}",
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
@@ -121,14 +121,18 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors = {"base": ex.error_code}
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=f"{self.discovery.product_name} ({self.discovery.serial})",
|
||||
title=self.discovery.product_name,
|
||||
data={CONF_IP_ADDRESS: self.discovery.ip},
|
||||
)
|
||||
|
||||
self._set_confirm_only()
|
||||
self.context["title_placeholders"] = {
|
||||
"name": f"{self.discovery.product_name} ({self.discovery.serial})"
|
||||
}
|
||||
|
||||
# We won't be adding mac/serial to the title for devices
|
||||
# that users generally don't have multiple of.
|
||||
name = self.discovery.product_name
|
||||
if self.discovery.product_type not in ["HWE-P1", "HWE-WTR"]:
|
||||
name = f"{name} ({self.discovery.serial})"
|
||||
self.context["title_placeholders"] = {"name": name}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
|
||||
@@ -28,18 +28,23 @@ async def async_get_config_entry_diagnostics(
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
meter_data = {
|
||||
"device": asdict(coordinator.data.device),
|
||||
"data": asdict(coordinator.data.data),
|
||||
"state": asdict(coordinator.data.state)
|
||||
if coordinator.data.state is not None
|
||||
else None,
|
||||
"system": asdict(coordinator.data.system)
|
||||
if coordinator.data.system is not None
|
||||
else None,
|
||||
}
|
||||
state: dict[str, Any] | None = None
|
||||
if coordinator.data.state:
|
||||
state = asdict(coordinator.data.state)
|
||||
|
||||
return {
|
||||
"entry": async_redact_data(entry.data, TO_REDACT),
|
||||
"data": async_redact_data(meter_data, TO_REDACT),
|
||||
}
|
||||
system: dict[str, Any] | None = None
|
||||
if coordinator.data.system:
|
||||
system = asdict(coordinator.data.system)
|
||||
|
||||
return async_redact_data(
|
||||
{
|
||||
"entry": async_redact_data(entry.data, TO_REDACT),
|
||||
"data": {
|
||||
"device": asdict(coordinator.data.device),
|
||||
"data": asdict(coordinator.data.data),
|
||||
"state": state,
|
||||
"system": system,
|
||||
},
|
||||
},
|
||||
TO_REDACT,
|
||||
)
|
||||
|
||||
@@ -18,17 +18,13 @@ class HomeWizardEntity(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator]):
|
||||
"""Initialize the HomeWizard entity."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=coordinator.entry.title,
|
||||
manufacturer="HomeWizard",
|
||||
sw_version=coordinator.data.device.firmware_version,
|
||||
model=coordinator.data.device.product_type,
|
||||
)
|
||||
|
||||
if coordinator.data.device.serial is not None:
|
||||
if (serial_number := coordinator.data.device.serial) is not None:
|
||||
self._attr_device_info[ATTR_CONNECTIONS] = {
|
||||
(CONNECTION_NETWORK_MAC, coordinator.data.device.serial)
|
||||
}
|
||||
|
||||
self._attr_device_info[ATTR_IDENTIFIERS] = {
|
||||
(DOMAIN, coordinator.data.device.serial)
|
||||
(CONNECTION_NETWORK_MAC, serial_number)
|
||||
}
|
||||
self._attr_device_info[ATTR_IDENTIFIERS] = {(DOMAIN, serial_number)}
|
||||
|
||||
@@ -47,13 +47,17 @@ class HWEnergyNumberEntity(HomeWizardEntity, NumberEntity):
|
||||
await self.coordinator.api.state_set(brightness=int(value * (255 / 100)))
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.coordinator.data.state is not None
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the current value."""
|
||||
if (
|
||||
self.coordinator.data.state is None
|
||||
or self.coordinator.data.state.brightness is None
|
||||
not self.coordinator.data.state
|
||||
or (brightness := self.coordinator.data.state.brightness) is None
|
||||
):
|
||||
return None
|
||||
brightness: float = self.coordinator.data.state.brightness
|
||||
return round(brightness * (100 / 255))
|
||||
|
||||
@@ -38,6 +38,7 @@ from .const import (
|
||||
CONF_COOL_AWAY_TEMPERATURE,
|
||||
CONF_HEAT_AWAY_TEMPERATURE,
|
||||
DOMAIN,
|
||||
RETRY,
|
||||
)
|
||||
|
||||
ATTR_FAN_ACTION = "fan_action"
|
||||
@@ -155,6 +156,7 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
self._cool_away_temp = cool_away_temp
|
||||
self._heat_away_temp = heat_away_temp
|
||||
self._away = False
|
||||
self._retry = 0
|
||||
|
||||
self._attr_unique_id = device.deviceid
|
||||
|
||||
@@ -483,21 +485,28 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
try:
|
||||
await self._device.refresh()
|
||||
self._attr_available = True
|
||||
self._retry = 0
|
||||
|
||||
except UnauthorizedError:
|
||||
try:
|
||||
await self._data.client.login()
|
||||
await self._device.refresh()
|
||||
self._attr_available = True
|
||||
self._retry = 0
|
||||
|
||||
except (
|
||||
SomeComfortError,
|
||||
ClientConnectionError,
|
||||
asyncio.TimeoutError,
|
||||
):
|
||||
self._attr_available = False
|
||||
self._retry += 1
|
||||
if self._retry > RETRY:
|
||||
self._attr_available = False
|
||||
|
||||
except (ClientConnectionError, asyncio.TimeoutError):
|
||||
self._attr_available = False
|
||||
self._retry += 1
|
||||
if self._retry > RETRY:
|
||||
self._attr_available = False
|
||||
|
||||
except UnexpectedResponse:
|
||||
pass
|
||||
|
||||
@@ -10,3 +10,4 @@ DEFAULT_HEAT_AWAY_TEMPERATURE = 61
|
||||
CONF_DEV_ID = "thermostat"
|
||||
CONF_LOC_ID = "location"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
RETRY = 3
|
||||
|
||||
@@ -146,7 +146,7 @@ SENSORS_INFO = [
|
||||
translation_key="energy_today",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
key=SOURCE_TYPE_ELECTRICITY,
|
||||
sensor_type=SENSOR_TYPE_THIS_DAY,
|
||||
precision=1,
|
||||
@@ -156,7 +156,7 @@ SENSORS_INFO = [
|
||||
translation_key="energy_week",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
key=SOURCE_TYPE_ELECTRICITY,
|
||||
sensor_type=SENSOR_TYPE_THIS_WEEK,
|
||||
precision=1,
|
||||
@@ -166,7 +166,7 @@ SENSORS_INFO = [
|
||||
translation_key="energy_month",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
key=SOURCE_TYPE_ELECTRICITY,
|
||||
sensor_type=SENSOR_TYPE_THIS_MONTH,
|
||||
precision=1,
|
||||
@@ -176,7 +176,7 @@ SENSORS_INFO = [
|
||||
translation_key="energy_year",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
key=SOURCE_TYPE_ELECTRICITY,
|
||||
sensor_type=SENSOR_TYPE_THIS_YEAR,
|
||||
precision=1,
|
||||
@@ -197,7 +197,7 @@ SENSORS_INFO = [
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
key=SOURCE_TYPE_GAS,
|
||||
sensor_type=SENSOR_TYPE_THIS_DAY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
icon="mdi:counter",
|
||||
precision=1,
|
||||
),
|
||||
@@ -207,7 +207,7 @@ SENSORS_INFO = [
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
key=SOURCE_TYPE_GAS,
|
||||
sensor_type=SENSOR_TYPE_THIS_WEEK,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
icon="mdi:counter",
|
||||
precision=1,
|
||||
),
|
||||
@@ -217,7 +217,7 @@ SENSORS_INFO = [
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
key=SOURCE_TYPE_GAS,
|
||||
sensor_type=SENSOR_TYPE_THIS_MONTH,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
icon="mdi:counter",
|
||||
precision=1,
|
||||
),
|
||||
@@ -227,7 +227,7 @@ SENSORS_INFO = [
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
key=SOURCE_TYPE_GAS,
|
||||
sensor_type=SENSOR_TYPE_THIS_YEAR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
icon="mdi:counter",
|
||||
precision=1,
|
||||
),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
|
||||
from pydrawise import legacy
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
@@ -13,11 +12,10 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
||||
from .const import DOMAIN, SCAN_INTERVAL
|
||||
from .coordinator import HydrawiseDataUpdateCoordinator
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@@ -53,24 +51,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up Hydrawise from a config entry."""
|
||||
access_token = config_entry.data[CONF_API_KEY]
|
||||
try:
|
||||
hydrawise = await hass.async_add_executor_job(
|
||||
legacy.LegacyHydrawise, access_token
|
||||
)
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
LOGGER.error("Unable to connect to Hydrawise cloud service: %s", str(ex))
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unable to connect to Hydrawise cloud service: {ex}"
|
||||
) from ex
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[
|
||||
config_entry.entry_id
|
||||
] = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL)
|
||||
if not hydrawise.controller_info or not hydrawise.controller_status:
|
||||
raise ConfigEntryNotReady("Hydrawise data not loaded")
|
||||
|
||||
# NOTE: We don't need to call async_config_entry_first_refresh() because
|
||||
# data is fetched when the Hydrawiser object is instantiated.
|
||||
hydrawise = legacy.LegacyHydrawise(access_token, load_on_init=False)
|
||||
coordinator = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ from homeassistant.components.binary_sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HydrawiseDataUpdateCoordinator
|
||||
from .entity import HydrawiseEntity
|
||||
|
||||
@@ -95,13 +95,10 @@ async def async_setup_entry(
|
||||
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
|
||||
"""A sensor implementation for Hydrawise device."""
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Get the latest data and updates the state."""
|
||||
LOGGER.debug("Updating Hydrawise binary sensor: %s", self.name)
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update state attributes."""
|
||||
if self.entity_description.key == "status":
|
||||
self._attr_is_on = self.coordinator.last_update_success
|
||||
elif self.entity_description.key == "is_watering":
|
||||
relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]]
|
||||
self._attr_is_on = relay_data["timestr"] == "Now"
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -36,3 +37,14 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]):
|
||||
name=data["name"],
|
||||
manufacturer=MANUFACTURER,
|
||||
)
|
||||
self._update_attrs()
|
||||
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update state attributes."""
|
||||
return # pragma: no cover
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Get the latest data and updates the state."""
|
||||
self._update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@@ -11,13 +11,13 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HydrawiseDataUpdateCoordinator
|
||||
from .entity import HydrawiseEntity
|
||||
|
||||
@@ -82,10 +82,8 @@ async def async_setup_entry(
|
||||
class HydrawiseSensor(HydrawiseEntity, SensorEntity):
|
||||
"""A sensor implementation for Hydrawise device."""
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Get the latest data and updates the states."""
|
||||
LOGGER.debug("Updating Hydrawise sensor: %s", self.name)
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update state attributes."""
|
||||
relay_data = self.coordinator.api.relays_by_zone_number[self.data["relay"]]
|
||||
if self.entity_description.key == "watering_time":
|
||||
if relay_data["timestr"] == "Now":
|
||||
@@ -94,8 +92,6 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity):
|
||||
self._attr_native_value = 0
|
||||
else: # _sensor_type == 'next_cycle'
|
||||
next_cycle = min(relay_data["time"], TWO_YEAR_SECONDS)
|
||||
LOGGER.debug("New cycle time: %s", next_cycle)
|
||||
self._attr_native_value = dt_util.utc_from_timestamp(
|
||||
dt_util.as_timestamp(dt_util.now()) + next_cycle
|
||||
)
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.components.switch import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -23,7 +23,6 @@ from .const import (
|
||||
CONF_WATERING_TIME,
|
||||
DEFAULT_WATERING_TIME,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
from .coordinator import HydrawiseDataUpdateCoordinator
|
||||
from .entity import HydrawiseEntity
|
||||
@@ -124,14 +123,11 @@ class HydrawiseSwitch(HydrawiseEntity, SwitchEntity):
|
||||
elif self.entity_description.key == "auto_watering":
|
||||
self.coordinator.api.suspend_zone(365, zone_number)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Update device state."""
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update state attributes."""
|
||||
zone_number = self.data["relay"]
|
||||
LOGGER.debug("Updating Hydrawise switch: %s", self.name)
|
||||
timestr = self.coordinator.api.relays_by_zone_number[zone_number]["timestr"]
|
||||
if self.entity_description.key == "manual_watering":
|
||||
self._attr_is_on = timestr == "Now"
|
||||
elif self.entity_description.key == "auto_watering":
|
||||
self._attr_is_on = timestr not in {"", "Now"}
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"xknx==2.11.2",
|
||||
"xknxproject==3.3.0",
|
||||
"xknxproject==3.4.0",
|
||||
"knx-frontend==2023.6.23.191712"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -149,31 +149,29 @@ async def _async_reproduce_state(
|
||||
service = SERVICE_TURN_ON
|
||||
for attr in ATTR_GROUP:
|
||||
# All attributes that are not colors
|
||||
if attr in state.attributes:
|
||||
service_data[attr] = state.attributes[attr]
|
||||
if (attr_state := state.attributes.get(attr)) is not None:
|
||||
service_data[attr] = attr_state
|
||||
|
||||
if (
|
||||
state.attributes.get(ATTR_COLOR_MODE, ColorMode.UNKNOWN)
|
||||
!= ColorMode.UNKNOWN
|
||||
):
|
||||
color_mode = state.attributes[ATTR_COLOR_MODE]
|
||||
if color_mode_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode):
|
||||
if color_mode_attr.state_attr not in state.attributes:
|
||||
if cm_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode):
|
||||
if (cm_attr_state := state.attributes.get(cm_attr.state_attr)) is None:
|
||||
_LOGGER.warning(
|
||||
"Color mode %s specified but attribute %s missing for: %s",
|
||||
color_mode,
|
||||
color_mode_attr.state_attr,
|
||||
cm_attr.state_attr,
|
||||
state.entity_id,
|
||||
)
|
||||
return
|
||||
service_data[color_mode_attr.parameter] = state.attributes[
|
||||
color_mode_attr.state_attr
|
||||
]
|
||||
service_data[cm_attr.parameter] = cm_attr_state
|
||||
else:
|
||||
# Fall back to Choosing the first color that is specified
|
||||
for color_attr in COLOR_GROUP:
|
||||
if color_attr in state.attributes:
|
||||
service_data[color_attr] = state.attributes[color_attr]
|
||||
if (color_attr_state := state.attributes.get(color_attr)) is not None:
|
||||
service_data[color_attr] = color_attr_state
|
||||
break
|
||||
|
||||
elif state.state == STATE_OFF:
|
||||
|
||||
@@ -139,20 +139,28 @@ class LocalTodoListEntity(TodoListEntity):
|
||||
await self._async_save()
|
||||
await self.async_update_ha_state(force_refresh=True)
|
||||
|
||||
async def async_move_todo_item(self, uid: str, pos: int) -> None:
|
||||
async def async_move_todo_item(
|
||||
self, uid: str, previous_uid: str | None = None
|
||||
) -> None:
|
||||
"""Re-order an item to the To-do list."""
|
||||
if uid == previous_uid:
|
||||
return
|
||||
todos = self._calendar.todos
|
||||
found_item: Todo | None = None
|
||||
for idx, itm in enumerate(todos):
|
||||
if itm.uid == uid:
|
||||
found_item = itm
|
||||
todos.pop(idx)
|
||||
break
|
||||
if found_item is None:
|
||||
item_idx: dict[str, int] = {itm.uid: idx for idx, itm in enumerate(todos)}
|
||||
if uid not in item_idx:
|
||||
raise HomeAssistantError(
|
||||
f"Item '{uid}' not found in todo list {self.entity_id}"
|
||||
"Item '{uid}' not found in todo list {self.entity_id}"
|
||||
)
|
||||
todos.insert(pos, found_item)
|
||||
if previous_uid and previous_uid not in item_idx:
|
||||
raise HomeAssistantError(
|
||||
"Item '{previous_uid}' not found in todo list {self.entity_id}"
|
||||
)
|
||||
dst_idx = item_idx[previous_uid] + 1 if previous_uid else 0
|
||||
src_idx = item_idx[uid]
|
||||
src_item = todos.pop(src_idx)
|
||||
if dst_idx > src_idx:
|
||||
dst_idx -= 1
|
||||
todos.insert(dst_idx, src_item)
|
||||
await self._async_save()
|
||||
await self.async_update_ha_state(force_refresh=True)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_NAME, CONF_WEBHOOK_ID
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_NAME, CONF_WEBHOOK_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -95,7 +95,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# Check if already exists
|
||||
await self.async_set_unique_id(lock_data["bridge_mac_wifi"])
|
||||
self._abort_if_unique_id_configured({CONF_HOST: host})
|
||||
self._abort_if_unique_id_configured({"bridge_ip": host})
|
||||
|
||||
return await self.async_step_user()
|
||||
|
||||
|
||||
@@ -60,6 +60,10 @@ async def websocket_lovelace_resources(
|
||||
"""Send Lovelace UI resources over WebSocket configuration."""
|
||||
resources = hass.data[DOMAIN]["resources"]
|
||||
|
||||
if hass.config.safe_mode:
|
||||
connection.send_result(msg["id"], [])
|
||||
return
|
||||
|
||||
if not resources.loaded:
|
||||
await resources.async_load()
|
||||
resources.loaded = True
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -68,6 +69,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
await cleanup_old_device(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -88,6 +91,15 @@ async def async_update_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def cleanup_old_device(hass: HomeAssistant) -> None:
|
||||
"""Cleanup device without proper device identifier."""
|
||||
device_reg = dr.async_get(hass)
|
||||
device = device_reg.async_get_device(identifiers={(DOMAIN,)}) # type: ignore[arg-type]
|
||||
if device:
|
||||
_LOGGER.debug("Removing improper device %s", device.name)
|
||||
device_reg.async_remove_device(device.id)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Unable to connect to the web site."""
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ async def async_setup_entry(
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(name, str)
|
||||
|
||||
entities = [MetWeather(coordinator, config_entry.data, False, name, is_metric)]
|
||||
entities = [MetWeather(coordinator, config_entry, False, name, is_metric)]
|
||||
|
||||
# Add hourly entity to legacy config entries
|
||||
if entity_registry.async_get_entity_id(
|
||||
@@ -69,9 +69,7 @@ async def async_setup_entry(
|
||||
_calculate_unique_id(config_entry.data, True),
|
||||
):
|
||||
name = f"{name} hourly"
|
||||
entities.append(
|
||||
MetWeather(coordinator, config_entry.data, True, name, is_metric)
|
||||
)
|
||||
entities.append(MetWeather(coordinator, config_entry, True, name, is_metric))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -114,22 +112,22 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: MetDataUpdateCoordinator,
|
||||
config: MappingProxyType[str, Any],
|
||||
config_entry: ConfigEntry,
|
||||
hourly: bool,
|
||||
name: str,
|
||||
is_metric: bool,
|
||||
) -> None:
|
||||
"""Initialise the platform with a data instance and site."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = _calculate_unique_id(config, hourly)
|
||||
self._config = config
|
||||
self._attr_unique_id = _calculate_unique_id(config_entry.data, hourly)
|
||||
self._config = config_entry.data
|
||||
self._is_metric = is_metric
|
||||
self._hourly = hourly
|
||||
self._attr_entity_registry_enabled_default = not hourly
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name="Forecast",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN,)}, # type: ignore[arg-type]
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer="Met.no",
|
||||
model="Forecast",
|
||||
configuration_url="https://www.met.no/en",
|
||||
|
||||
@@ -47,6 +47,7 @@ from .client import ( # noqa: F401
|
||||
publish,
|
||||
subscribe,
|
||||
)
|
||||
from .config import MQTT_BASE_SCHEMA, MQTT_RO_SCHEMA, MQTT_RW_SCHEMA # noqa: F401
|
||||
from .config_integration import CONFIG_SCHEMA_BASE
|
||||
from .const import ( # noqa: F401
|
||||
ATTR_PAYLOAD,
|
||||
@@ -232,7 +233,7 @@ async def async_check_config_schema(
|
||||
) -> None:
|
||||
"""Validate manually configured MQTT items."""
|
||||
mqtt_data = get_mqtt_data(hass)
|
||||
mqtt_config: list[dict[str, list[ConfigType]]] = config_yaml[DOMAIN]
|
||||
mqtt_config: list[dict[str, list[ConfigType]]] = config_yaml.get(DOMAIN, {})
|
||||
for mqtt_config_item in mqtt_config:
|
||||
for domain, config_items in mqtt_config_item.items():
|
||||
schema = mqtt_data.reload_schema[domain]
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
},
|
||||
"qos": {
|
||||
"name": "QoS",
|
||||
"description": "Quality of Service to use. O. At most once. 1: At least once. 2: Exactly once."
|
||||
"description": "Quality of Service to use. 0: At most once. 1: At least once. 2: Exactly once."
|
||||
},
|
||||
"retain": {
|
||||
"name": "Retain",
|
||||
|
||||
@@ -47,7 +47,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
).extend(mqtt.config.MQTT_RO_SCHEMA.schema)
|
||||
).extend(mqtt.MQTT_RO_SCHEMA.schema)
|
||||
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["google_nest_sdm"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["google-nest-sdm==3.0.2"]
|
||||
"requirements": ["google-nest-sdm==3.0.3"]
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.components.recorder.statistics import (
|
||||
statistics_during_period,
|
||||
)
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
@@ -58,6 +58,16 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
entry_data.get(CONF_TOTP_SECRET),
|
||||
)
|
||||
|
||||
@callback
|
||||
def _dummy_listener() -> None:
|
||||
pass
|
||||
|
||||
# Force the coordinator to periodically update by registering at least one listener.
|
||||
# Needed when the _async_update_data below returns {} for utilities that don't provide
|
||||
# forecast, which results to no sensors added, no registered listeners, and thus
|
||||
# _async_update_data not periodically getting called which is needed for _insert_statistics.
|
||||
self.async_add_listener(_dummy_listener)
|
||||
|
||||
async def _async_update_data(
|
||||
self,
|
||||
) -> dict[str, Forecast]:
|
||||
@@ -71,6 +81,8 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
raise ConfigEntryAuthFailed from err
|
||||
forecasts: list[Forecast] = await self.api.async_get_forecast()
|
||||
_LOGGER.debug("Updating sensor data with: %s", forecasts)
|
||||
# Because Opower provides historical usage/cost with a delay of a couple of days
|
||||
# we need to insert data into statistics.
|
||||
await self._insert_statistics()
|
||||
return {forecast.account.utility_account_id: forecast for forecast in forecasts}
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.0.37"]
|
||||
"requirements": ["opower==0.0.38"]
|
||||
}
|
||||
|
||||
@@ -70,25 +70,25 @@ def async_setup_proximity_component(
|
||||
ignored_zones: list[str] = config[CONF_IGNORED_ZONES]
|
||||
proximity_devices: list[str] = config[CONF_DEVICES]
|
||||
tolerance: int = config[CONF_TOLERANCE]
|
||||
proximity_zone = name
|
||||
proximity_zone = config[CONF_ZONE]
|
||||
unit_of_measurement: str = config.get(
|
||||
CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit
|
||||
)
|
||||
zone_id = f"zone.{config[CONF_ZONE]}"
|
||||
zone_friendly_name = name
|
||||
|
||||
proximity = Proximity(
|
||||
hass,
|
||||
proximity_zone,
|
||||
zone_friendly_name,
|
||||
DEFAULT_DIST_TO_ZONE,
|
||||
DEFAULT_DIR_OF_TRAVEL,
|
||||
DEFAULT_NEAREST,
|
||||
ignored_zones,
|
||||
proximity_devices,
|
||||
tolerance,
|
||||
zone_id,
|
||||
proximity_zone,
|
||||
unit_of_measurement,
|
||||
)
|
||||
proximity.entity_id = f"{DOMAIN}.{proximity_zone}"
|
||||
proximity.entity_id = f"{DOMAIN}.{zone_friendly_name}"
|
||||
|
||||
proximity.async_write_ha_state()
|
||||
|
||||
@@ -171,7 +171,7 @@ class Proximity(Entity):
|
||||
devices_to_calculate = False
|
||||
devices_in_zone = ""
|
||||
|
||||
zone_state = self.hass.states.get(self.proximity_zone)
|
||||
zone_state = self.hass.states.get(f"zone.{self.proximity_zone}")
|
||||
proximity_latitude = (
|
||||
zone_state.attributes.get(ATTR_LATITUDE) if zone_state else None
|
||||
)
|
||||
@@ -189,7 +189,7 @@ class Proximity(Entity):
|
||||
devices_to_calculate = True
|
||||
|
||||
# Check the location of all devices.
|
||||
if (device_state.state).lower() == (self.friendly_name).lower():
|
||||
if (device_state.state).lower() == (self.proximity_zone).lower():
|
||||
device_friendly = device_state.name
|
||||
if devices_in_zone != "":
|
||||
devices_in_zone = f"{devices_in_zone}, "
|
||||
|
||||
@@ -18,5 +18,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"requirements": ["reolink-aio==0.7.11"]
|
||||
"requirements": ["reolink-aio==0.7.12"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/schlage",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pyschlage==2023.9.1"]
|
||||
"requirements": ["pyschlage==2023.10.0"]
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/screenlogic",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["screenlogicpy"],
|
||||
"requirements": ["screenlogicpy==0.9.3"]
|
||||
"requirements": ["screenlogicpy==0.9.4"]
|
||||
}
|
||||
|
||||
@@ -322,17 +322,23 @@ class ShoppingData:
|
||||
context=context,
|
||||
)
|
||||
|
||||
async def async_move_item(self, uid: str, pos: int) -> None:
|
||||
async def async_move_item(self, uid: str, previous: str | None = None) -> None:
|
||||
"""Re-order a shopping list item."""
|
||||
found_item: dict[str, Any] | None = None
|
||||
for idx, itm in enumerate(self.items):
|
||||
if cast(str, itm["id"]) == uid:
|
||||
found_item = itm
|
||||
self.items.pop(idx)
|
||||
break
|
||||
if not found_item:
|
||||
if uid == previous:
|
||||
return
|
||||
item_idx = {cast(str, itm["id"]): idx for idx, itm in enumerate(self.items)}
|
||||
if uid not in item_idx:
|
||||
raise NoMatchingShoppingListItem(f"Item '{uid}' not found in shopping list")
|
||||
self.items.insert(pos, found_item)
|
||||
if previous and previous not in item_idx:
|
||||
raise NoMatchingShoppingListItem(
|
||||
f"Item '{previous}' not found in shopping list"
|
||||
)
|
||||
dst_idx = item_idx[previous] + 1 if previous else 0
|
||||
src_idx = item_idx[uid]
|
||||
src_item = self.items.pop(src_idx)
|
||||
if dst_idx > src_idx:
|
||||
dst_idx -= 1
|
||||
self.items.insert(dst_idx, src_item)
|
||||
await self.hass.async_add_executor_job(self.save)
|
||||
self._async_notify()
|
||||
self.hass.bus.async_fire(
|
||||
|
||||
@@ -71,11 +71,13 @@ class ShoppingTodoListEntity(TodoListEntity):
|
||||
"""Add an item to the To-do list."""
|
||||
await self._data.async_remove_items(set(uids))
|
||||
|
||||
async def async_move_todo_item(self, uid: str, pos: int) -> None:
|
||||
async def async_move_todo_item(
|
||||
self, uid: str, previous_uid: str | None = None
|
||||
) -> None:
|
||||
"""Re-order an item to the To-do list."""
|
||||
|
||||
try:
|
||||
await self._data.async_move_item(uid, pos)
|
||||
await self._data.async_move_item(uid, previous_uid)
|
||||
except NoMatchingShoppingListItem as err:
|
||||
raise HomeAssistantError(
|
||||
f"Shopping list item '{uid}' could not be re-ordered"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/starlink",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["starlink-grpc-core==1.1.2"]
|
||||
"requirements": ["starlink-grpc-core==1.1.3"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/subaru",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["stdiomask", "subarulink"],
|
||||
"requirements": ["subarulink==0.7.6"]
|
||||
"requirements": ["subarulink==0.7.8"]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
|
||||
from . import SwitchbotCloudData
|
||||
from .const import DOMAIN
|
||||
@@ -44,7 +43,6 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
@@ -55,7 +53,10 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class SwitchBotCloudAirConditionner(SwitchBotCloudEntity, ClimateEntity):
|
||||
"""Representation of a SwitchBot air conditionner, as it is an IR device, we don't know the actual state."""
|
||||
"""Representation of a SwitchBot air conditionner.
|
||||
|
||||
As it is an IR device, we don't know the actual state.
|
||||
"""
|
||||
|
||||
_attr_assumed_state = True
|
||||
_attr_supported_features = (
|
||||
@@ -116,3 +117,4 @@ class SwitchBotCloudAirConditionner(SwitchBotCloudEntity, ClimateEntity):
|
||||
return
|
||||
await self._do_send_command(temperature=temperature)
|
||||
self._attr_target_temperature = temperature
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -7,7 +7,6 @@ from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
|
||||
from . import SwitchbotCloudData
|
||||
from .const import DOMAIN
|
||||
@@ -19,7 +18,6 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||
|
||||
@@ -119,7 +119,7 @@ TADO_MODES_TO_HA_CURRENT_HVAC_ACTION = {
|
||||
}
|
||||
|
||||
# These modes will not allow a temp to be set
|
||||
TADO_MODES_WITH_NO_TEMP_SETTING = [CONST_MODE_AUTO, CONST_MODE_DRY, CONST_MODE_FAN]
|
||||
TADO_MODES_WITH_NO_TEMP_SETTING = [CONST_MODE_AUTO, CONST_MODE_FAN]
|
||||
#
|
||||
# HVAC_MODE_HEAT_COOL is mapped to CONST_MODE_AUTO
|
||||
# This lets tado decide on a temp
|
||||
|
||||
@@ -43,14 +43,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
websocket_api.async_register_command(hass, websocket_handle_todo_item_move)
|
||||
|
||||
component.async_register_entity_service(
|
||||
"create_item",
|
||||
"add_item",
|
||||
{
|
||||
vol.Required("summary"): vol.All(cv.string, vol.Length(min=1)),
|
||||
vol.Optional("status", default=TodoItemStatus.NEEDS_ACTION): vol.In(
|
||||
{TodoItemStatus.NEEDS_ACTION, TodoItemStatus.COMPLETED}
|
||||
),
|
||||
vol.Required("item"): vol.All(cv.string, vol.Length(min=1)),
|
||||
},
|
||||
_async_create_todo_item,
|
||||
_async_add_todo_item,
|
||||
required_features=[TodoListEntityFeature.CREATE_TODO_ITEM],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
@@ -58,30 +55,26 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
vol.All(
|
||||
cv.make_entity_service_schema(
|
||||
{
|
||||
vol.Optional("uid"): cv.string,
|
||||
vol.Optional("summary"): vol.All(cv.string, vol.Length(min=1)),
|
||||
vol.Required("item"): vol.All(cv.string, vol.Length(min=1)),
|
||||
vol.Optional("rename"): vol.All(cv.string, vol.Length(min=1)),
|
||||
vol.Optional("status"): vol.In(
|
||||
{TodoItemStatus.NEEDS_ACTION, TodoItemStatus.COMPLETED}
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key("uid", "summary"),
|
||||
cv.has_at_least_one_key("rename", "status"),
|
||||
),
|
||||
_async_update_todo_item,
|
||||
required_features=[TodoListEntityFeature.UPDATE_TODO_ITEM],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
"delete_item",
|
||||
vol.All(
|
||||
cv.make_entity_service_schema(
|
||||
{
|
||||
vol.Optional("uid"): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional("summary"): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key("uid", "summary"),
|
||||
"remove_item",
|
||||
cv.make_entity_service_schema(
|
||||
{
|
||||
vol.Required("item"): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
),
|
||||
_async_delete_todo_items,
|
||||
_async_remove_todo_items,
|
||||
required_features=[TodoListEntityFeature.DELETE_TODO_ITEM],
|
||||
)
|
||||
|
||||
@@ -114,13 +107,6 @@ class TodoItem:
|
||||
status: TodoItemStatus | None = None
|
||||
"""A status or confirmation of the To-do item."""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, obj: dict[str, Any]) -> "TodoItem":
|
||||
"""Create a To-do Item from a dictionary parsed by schema validators."""
|
||||
return cls(
|
||||
summary=obj.get("summary"), status=obj.get("status"), uid=obj.get("uid")
|
||||
)
|
||||
|
||||
|
||||
class TodoListEntity(Entity):
|
||||
"""An entity that represents a To-do list."""
|
||||
@@ -152,8 +138,15 @@ class TodoListEntity(Entity):
|
||||
"""Delete an item in the To-do list."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def async_move_todo_item(self, uid: str, pos: int) -> None:
|
||||
"""Move an item in the To-do list."""
|
||||
async def async_move_todo_item(
|
||||
self, uid: str, previous_uid: str | None = None
|
||||
) -> None:
|
||||
"""Move an item in the To-do list.
|
||||
|
||||
The To-do item with the specified `uid` should be moved to the position
|
||||
in the list after the specified by `previous_uid` or `None` for the first
|
||||
position in the To-do list.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@@ -190,7 +183,7 @@ async def websocket_handle_todo_item_list(
|
||||
vol.Required("type"): "todo/item/move",
|
||||
vol.Required("entity_id"): cv.entity_id,
|
||||
vol.Required("uid"): cv.string,
|
||||
vol.Optional("pos", default=0): cv.positive_int,
|
||||
vol.Optional("previous_uid"): cv.string,
|
||||
}
|
||||
)
|
||||
@websocket_api.async_response
|
||||
@@ -215,48 +208,53 @@ async def websocket_handle_todo_item_move(
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await entity.async_move_todo_item(uid=msg["uid"], pos=msg["pos"])
|
||||
await entity.async_move_todo_item(
|
||||
uid=msg["uid"], previous_uid=msg.get("previous_uid")
|
||||
)
|
||||
except HomeAssistantError as ex:
|
||||
connection.send_error(msg["id"], "failed", str(ex))
|
||||
else:
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
def _find_by_summary(summary: str, items: list[TodoItem] | None) -> TodoItem | None:
|
||||
"""Find a To-do List item by summary name."""
|
||||
def _find_by_uid_or_summary(
|
||||
value: str, items: list[TodoItem] | None
|
||||
) -> TodoItem | None:
|
||||
"""Find a To-do List item by uid or summary name."""
|
||||
for item in items or ():
|
||||
if item.summary == summary:
|
||||
if value in (item.uid, item.summary):
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
async def _async_create_todo_item(entity: TodoListEntity, call: ServiceCall) -> None:
|
||||
async def _async_add_todo_item(entity: TodoListEntity, call: ServiceCall) -> None:
|
||||
"""Add an item to the To-do list."""
|
||||
await entity.async_create_todo_item(item=TodoItem.from_dict(call.data))
|
||||
await entity.async_create_todo_item(
|
||||
item=TodoItem(summary=call.data["item"], status=TodoItemStatus.NEEDS_ACTION)
|
||||
)
|
||||
|
||||
|
||||
async def _async_update_todo_item(entity: TodoListEntity, call: ServiceCall) -> None:
|
||||
"""Update an item in the To-do list."""
|
||||
item = TodoItem.from_dict(call.data)
|
||||
if not item.uid:
|
||||
found = _find_by_summary(call.data["summary"], entity.todo_items)
|
||||
if not found:
|
||||
raise ValueError(f"Unable to find To-do item with summary '{item.summary}'")
|
||||
item.uid = found.uid
|
||||
item = call.data["item"]
|
||||
found = _find_by_uid_or_summary(item, entity.todo_items)
|
||||
if not found:
|
||||
raise ValueError(f"Unable to find To-do item '{item}'")
|
||||
|
||||
await entity.async_update_todo_item(item=item)
|
||||
update_item = TodoItem(
|
||||
uid=found.uid, summary=call.data.get("rename"), status=call.data.get("status")
|
||||
)
|
||||
|
||||
await entity.async_update_todo_item(item=update_item)
|
||||
|
||||
|
||||
async def _async_delete_todo_items(entity: TodoListEntity, call: ServiceCall) -> None:
|
||||
"""Delete an item in the To-do list."""
|
||||
uids = call.data.get("uid", [])
|
||||
if not uids:
|
||||
summaries = call.data.get("summary", [])
|
||||
for summary in summaries:
|
||||
item = _find_by_summary(summary, entity.todo_items)
|
||||
if not item:
|
||||
raise ValueError(f"Unable to find To-do item with summary '{summary}")
|
||||
uids.append(item.uid)
|
||||
async def _async_remove_todo_items(entity: TodoListEntity, call: ServiceCall) -> None:
|
||||
"""Remove an item in the To-do list."""
|
||||
uids = []
|
||||
for item in call.data.get("item", []):
|
||||
found = _find_by_uid_or_summary(item, entity.todo_items)
|
||||
if not found or not found.uid:
|
||||
raise ValueError(f"Unable to find To-do item '{item}")
|
||||
uids.append(found.uid)
|
||||
await entity.async_delete_todo_items(uids=uids)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "todo",
|
||||
"name": "To-do",
|
||||
"name": "To-do list",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"dependencies": ["http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/todo",
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
create_item:
|
||||
add_item:
|
||||
target:
|
||||
entity:
|
||||
domain: todo
|
||||
supported_features:
|
||||
- todo.TodoListEntityFeature.CREATE_TODO_ITEM
|
||||
fields:
|
||||
summary:
|
||||
item:
|
||||
required: true
|
||||
example: "Submit Income Tax Return"
|
||||
example: "Submit income tax return"
|
||||
selector:
|
||||
text:
|
||||
status:
|
||||
example: "needs_action"
|
||||
selector:
|
||||
select:
|
||||
translation_key: status
|
||||
options:
|
||||
- needs_action
|
||||
- completed
|
||||
update_item:
|
||||
target:
|
||||
entity:
|
||||
@@ -25,11 +17,13 @@ update_item:
|
||||
supported_features:
|
||||
- todo.TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
fields:
|
||||
uid:
|
||||
item:
|
||||
required: true
|
||||
example: "Submit income tax return"
|
||||
selector:
|
||||
text:
|
||||
summary:
|
||||
example: "Submit Income Tax Return"
|
||||
rename:
|
||||
example: "Something else"
|
||||
selector:
|
||||
text:
|
||||
status:
|
||||
@@ -40,16 +34,14 @@ update_item:
|
||||
options:
|
||||
- needs_action
|
||||
- completed
|
||||
delete_item:
|
||||
remove_item:
|
||||
target:
|
||||
entity:
|
||||
domain: todo
|
||||
supported_features:
|
||||
- todo.TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
fields:
|
||||
uid:
|
||||
item:
|
||||
required: true
|
||||
selector:
|
||||
object:
|
||||
summary:
|
||||
selector:
|
||||
object:
|
||||
text:
|
||||
|
||||
@@ -1,54 +1,46 @@
|
||||
{
|
||||
"title": "To-do List",
|
||||
"title": "To-do list",
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::todo::title%]"
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"create_item": {
|
||||
"name": "Create To-do List Item",
|
||||
"description": "Add a new To-do List Item.",
|
||||
"add_item": {
|
||||
"name": "Add to-do list item",
|
||||
"description": "Add a new to-do list item.",
|
||||
"fields": {
|
||||
"summary": {
|
||||
"name": "Summary",
|
||||
"description": "The short summary that represents the To-do item."
|
||||
},
|
||||
"status": {
|
||||
"name": "Status",
|
||||
"description": "A status or confirmation of the To-do item."
|
||||
"item": {
|
||||
"name": "Item name",
|
||||
"description": "The name that represents the to-do item."
|
||||
}
|
||||
}
|
||||
},
|
||||
"update_item": {
|
||||
"name": "Update To-do List Item",
|
||||
"description": "Update an existing To-do List Item based on either its Unique Id or Summary.",
|
||||
"name": "Update to-do list item",
|
||||
"description": "Update an existing to-do list item based on its name.",
|
||||
"fields": {
|
||||
"uid": {
|
||||
"name": "To-do Item Unique Id",
|
||||
"description": "Unique Identifier for the To-do List Item."
|
||||
"item": {
|
||||
"name": "Item name",
|
||||
"description": "The name for the to-do list item."
|
||||
},
|
||||
"summary": {
|
||||
"name": "Summary",
|
||||
"description": "The short summary that represents the To-do item."
|
||||
"rename": {
|
||||
"name": "Rename item",
|
||||
"description": "The new name of the to-do item"
|
||||
},
|
||||
"status": {
|
||||
"name": "Status",
|
||||
"description": "A status or confirmation of the To-do item."
|
||||
"name": "Set status",
|
||||
"description": "A status or confirmation of the to-do item."
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete_item": {
|
||||
"name": "Delete a To-do List Item",
|
||||
"description": "Delete an existing To-do List Item either by its Unique Id or Summary.",
|
||||
"remove_item": {
|
||||
"name": "Remove a to-do list item",
|
||||
"description": "Remove an existing to-do list item by its name.",
|
||||
"fields": {
|
||||
"uid": {
|
||||
"name": "To-do Item Unique Ids",
|
||||
"description": "Unique Identifiers for the To-do List Items."
|
||||
},
|
||||
"summary": {
|
||||
"name": "Summary",
|
||||
"description": "The short summary that represents the To-do item."
|
||||
"item": {
|
||||
"name": "Item name",
|
||||
"description": "The name for the to-do list items."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,7 +48,7 @@
|
||||
"selector": {
|
||||
"status": {
|
||||
"options": {
|
||||
"needs_action": "Needs Action",
|
||||
"needs_action": "Not completed",
|
||||
"completed": "Completed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
await api.get_tasks()
|
||||
except HTTPError as err:
|
||||
if err.response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
errors["base"] = "invalid_access_token"
|
||||
errors["base"] = "invalid_api_key"
|
||||
else:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
|
||||
@@ -169,5 +169,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["kasa"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-kasa[speedups]==0.5.3"]
|
||||
"requirements": ["python-kasa[speedups]==0.5.4"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Support for the Transmission BitTorrent client API."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
import re
|
||||
@@ -22,7 +21,6 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME,
|
||||
Platform,
|
||||
)
|
||||
@@ -69,7 +67,7 @@ MIGRATION_NAME_TO_KEY = {
|
||||
|
||||
SERVICE_BASE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Exclusive(CONF_ENTRY_ID, "identifier"): selector.ConfigEntrySelector(),
|
||||
vol.Required(CONF_ENTRY_ID): selector.ConfigEntrySelector(),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -135,7 +133,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
config_entry.add_update_listener(async_options_updated)
|
||||
|
||||
async def add_torrent(service: ServiceCall) -> None:
|
||||
"""Add new torrent to download."""
|
||||
@@ -244,10 +241,3 @@ async def get_api(
|
||||
except TransmissionError as error:
|
||||
_LOGGER.error(error)
|
||||
raise UnknownError from error
|
||||
|
||||
|
||||
async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Triggered by config entry options updates."""
|
||||
coordinator: TransmissionDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator.update_interval = timedelta(seconds=entry.options[CONF_SCAN_INTERVAL])
|
||||
await coordinator.async_request_refresh()
|
||||
|
||||
@@ -55,12 +55,9 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
for entry in self._async_current_entries():
|
||||
if (
|
||||
entry.data[CONF_HOST] == user_input[CONF_HOST]
|
||||
and entry.data[CONF_PORT] == user_input[CONF_PORT]
|
||||
):
|
||||
return self.async_abort(reason="already_configured")
|
||||
self._async_abort_entries_match(
|
||||
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
|
||||
)
|
||||
try:
|
||||
await get_api(self.hass, user_input)
|
||||
|
||||
|
||||
@@ -71,13 +71,13 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
data = self.api.session_stats()
|
||||
self.torrents = self.api.get_torrents()
|
||||
self._session = self.api.get_session()
|
||||
|
||||
self.check_completed_torrent()
|
||||
self.check_started_torrent()
|
||||
self.check_removed_torrent()
|
||||
except transmission_rpc.TransmissionError as err:
|
||||
raise UpdateFailed("Unable to connect to Transmission client") from err
|
||||
|
||||
self.check_completed_torrent()
|
||||
self.check_started_torrent()
|
||||
self.check_removed_torrent()
|
||||
|
||||
return data
|
||||
|
||||
def init_torrent_list(self) -> None:
|
||||
|
||||
@@ -30,9 +30,7 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configure options for Transmission",
|
||||
"data": {
|
||||
"scan_interval": "Update frequency",
|
||||
"limit": "Limit",
|
||||
"order": "Order"
|
||||
}
|
||||
|
||||
@@ -534,16 +534,25 @@ class UtilityMeterSensor(RestoreSensor):
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _async_reset_meter(self, event):
|
||||
"""Determine cycle - Helper function for larger than daily cycles."""
|
||||
async def _program_reset(self):
|
||||
"""Program the reset of the utility meter."""
|
||||
if self._cron_pattern is not None:
|
||||
tz = dt_util.get_time_zone(self.hass.config.time_zone)
|
||||
self.async_on_remove(
|
||||
async_track_point_in_time(
|
||||
self.hass,
|
||||
self._async_reset_meter,
|
||||
croniter(self._cron_pattern, dt_util.now()).get_next(datetime),
|
||||
croniter(self._cron_pattern, dt_util.now(tz)).get_next(
|
||||
datetime
|
||||
), # we need timezone for DST purposes (see issue #102984)
|
||||
)
|
||||
)
|
||||
|
||||
async def _async_reset_meter(self, event):
|
||||
"""Reset the utility meter status."""
|
||||
|
||||
await self._program_reset()
|
||||
|
||||
await self.async_reset_meter(self._tariff_entity)
|
||||
|
||||
async def async_reset_meter(self, entity_id):
|
||||
@@ -566,14 +575,7 @@ class UtilityMeterSensor(RestoreSensor):
|
||||
"""Handle entity which will be added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if self._cron_pattern is not None:
|
||||
self.async_on_remove(
|
||||
async_track_point_in_time(
|
||||
self.hass,
|
||||
self._async_reset_meter,
|
||||
croniter(self._cron_pattern, dt_util.now()).get_next(datetime),
|
||||
)
|
||||
)
|
||||
await self._program_reset()
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/vasttrafik",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["vasttrafik"],
|
||||
"requirements": ["vtjp==0.1.14"]
|
||||
"requirements": ["vtjp==0.2.1"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Support for Västtrafik public transport."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
import vasttrafik
|
||||
@@ -22,6 +22,9 @@ ATTR_ACCESSIBILITY = "accessibility"
|
||||
ATTR_DIRECTION = "direction"
|
||||
ATTR_LINE = "line"
|
||||
ATTR_TRACK = "track"
|
||||
ATTR_FROM = "from"
|
||||
ATTR_TO = "to"
|
||||
ATTR_DELAY = "delay"
|
||||
|
||||
CONF_DEPARTURES = "departures"
|
||||
CONF_FROM = "from"
|
||||
@@ -32,7 +35,6 @@ CONF_SECRET = "secret"
|
||||
|
||||
DEFAULT_DELAY = 0
|
||||
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
@@ -101,7 +103,7 @@ class VasttrafikDepartureSensor(SensorEntity):
|
||||
if location.isdecimal():
|
||||
station_info = {"station_name": location, "station_id": location}
|
||||
else:
|
||||
station_id = self._planner.location_name(location)[0]["id"]
|
||||
station_id = self._planner.location_name(location)[0]["gid"]
|
||||
station_info = {"station_name": location, "station_id": station_id}
|
||||
return station_info
|
||||
|
||||
@@ -143,20 +145,36 @@ class VasttrafikDepartureSensor(SensorEntity):
|
||||
self._attributes = {}
|
||||
else:
|
||||
for departure in self._departureboard:
|
||||
line = departure.get("sname")
|
||||
if "cancelled" in departure:
|
||||
service_journey = departure.get("serviceJourney", {})
|
||||
line = service_journey.get("line", {})
|
||||
|
||||
if departure.get("isCancelled"):
|
||||
continue
|
||||
if not self._lines or line in self._lines:
|
||||
if "rtTime" in departure:
|
||||
self._state = departure["rtTime"]
|
||||
if not self._lines or line.get("shortName") in self._lines:
|
||||
if "estimatedOtherwisePlannedTime" in departure:
|
||||
try:
|
||||
self._state = datetime.fromisoformat(
|
||||
departure["estimatedOtherwisePlannedTime"]
|
||||
).strftime("%H:%M")
|
||||
except ValueError:
|
||||
self._state = departure["estimatedOtherwisePlannedTime"]
|
||||
else:
|
||||
self._state = departure["time"]
|
||||
self._state = None
|
||||
|
||||
stop_point = departure.get("stopPoint", {})
|
||||
|
||||
params = {
|
||||
ATTR_ACCESSIBILITY: departure.get("accessibility"),
|
||||
ATTR_DIRECTION: departure.get("direction"),
|
||||
ATTR_LINE: departure.get("sname"),
|
||||
ATTR_TRACK: departure.get("track"),
|
||||
ATTR_ACCESSIBILITY: "wheelChair"
|
||||
if line.get("isWheelchairAccessible")
|
||||
else None,
|
||||
ATTR_DIRECTION: service_journey.get("direction"),
|
||||
ATTR_LINE: line.get("shortName"),
|
||||
ATTR_TRACK: stop_point.get("platform"),
|
||||
ATTR_FROM: stop_point.get("name"),
|
||||
ATTR_TO: self._heading["station_name"]
|
||||
if self._heading
|
||||
else "ANY",
|
||||
ATTR_DELAY: self._delay.seconds // 60 % 60,
|
||||
}
|
||||
|
||||
self._attributes = {k: v for k, v in params.items() if v}
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import velbusaio
|
||||
import velbusaio.controller
|
||||
from velbusaio.exceptions import VelbusConnectionFailed
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"velbus-packet",
|
||||
"velbus-protocol"
|
||||
],
|
||||
"requirements": ["velbus-aio==2023.10.1"],
|
||||
"requirements": ["velbus-aio==2023.10.2"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10CF",
|
||||
|
||||
@@ -4,7 +4,10 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from PyViCare.PyViCareUtils import PyViCareInvalidCredentialsError
|
||||
from PyViCare.PyViCareUtils import (
|
||||
PyViCareInvalidConfigurationError,
|
||||
PyViCareInvalidCredentialsError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -53,7 +56,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
await self.hass.async_add_executor_job(
|
||||
vicare_login, self.hass, user_input
|
||||
)
|
||||
except PyViCareInvalidCredentialsError:
|
||||
except (PyViCareInvalidConfigurationError, PyViCareInvalidCredentialsError):
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
return self.async_create_entry(title=VICARE_NAME, data=user_input)
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/vicare",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["PyViCare"],
|
||||
"requirements": ["PyViCare==2.25.0"]
|
||||
"requirements": ["PyViCare==2.28.1"]
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
"flow_title": "{name} ({host})",
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com",
|
||||
"description": "Set up ViCare integration. To generate client ID go to https://app.developer.viessmann.com",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"client_id": "[%key:common::config_flow::data::api_key%]",
|
||||
"client_id": "Client ID",
|
||||
"heating_type": "Heating type"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,15 +95,19 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
"""Update router data."""
|
||||
_LOGGER.debug("Polling Vodafone Station host: %s", self._host)
|
||||
try:
|
||||
logged = await self.api.login()
|
||||
except exceptions.CannotConnect as err:
|
||||
_LOGGER.warning("Connection error for %s", self._host)
|
||||
raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
|
||||
except exceptions.CannotAuthenticate as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
|
||||
if not logged:
|
||||
raise ConfigEntryAuthFailed
|
||||
try:
|
||||
await self.api.login()
|
||||
except exceptions.CannotAuthenticate as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except (
|
||||
exceptions.CannotConnect,
|
||||
exceptions.AlreadyLogged,
|
||||
exceptions.GenericLoginError,
|
||||
) as err:
|
||||
raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
|
||||
except (ConfigEntryAuthFailed, UpdateFailed):
|
||||
await self.api.close()
|
||||
raise
|
||||
|
||||
utc_point_in_time = dt_util.utcnow()
|
||||
data_devices = {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/vodafone_station",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiovodafone"],
|
||||
"requirements": ["aiovodafone==0.4.1"]
|
||||
"requirements": ["aiovodafone==0.4.2"]
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ from .messages import construct_event_message, construct_result_message
|
||||
|
||||
ALL_SERVICE_DESCRIPTIONS_JSON_CACHE = "websocket_api_all_service_descriptions_json"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_commands(
|
||||
@@ -132,7 +134,12 @@ def handle_subscribe_events(
|
||||
event_type = msg["event_type"]
|
||||
|
||||
if event_type not in SUBSCRIBE_ALLOWLIST and not connection.user.is_admin:
|
||||
raise Unauthorized
|
||||
_LOGGER.error(
|
||||
"Refusing to allow %s to subscribe to event %s",
|
||||
connection.user.name,
|
||||
event_type,
|
||||
)
|
||||
raise Unauthorized(user_id=connection.user.id)
|
||||
|
||||
if event_type == EVENT_STATE_CHANGED:
|
||||
forward_events = callback(
|
||||
|
||||
@@ -33,4 +33,6 @@ async def async_get_config_entry_diagnostics(
|
||||
"webhooks_connected": withings_data.measurement_coordinator.webhooks_connected,
|
||||
"received_measurements": list(withings_data.measurement_coordinator.data),
|
||||
"received_sleep_data": withings_data.sleep_coordinator.data is not None,
|
||||
"received_workout_data": withings_data.workout_coordinator.data is not None,
|
||||
"received_activity_data": withings_data.activity_coordinator.data is not None,
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiowithings"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiowithings==1.0.1"]
|
||||
"requirements": ["aiowithings==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ from homeassistant.util import dt as dt_util
|
||||
from . import WithingsData
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
SCORE_POINTS,
|
||||
UOM_BEATS_PER_MINUTE,
|
||||
UOM_BREATHS_PER_MINUTE,
|
||||
@@ -787,6 +788,11 @@ async def async_setup_entry(
|
||||
_async_add_workout_entities
|
||||
)
|
||||
|
||||
if not entities:
|
||||
LOGGER.warning(
|
||||
"No data found for Withings entry %s, sensors will be added when new data is available"
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
|
||||
@@ -259,11 +259,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
|
||||
def _current_mode_setpoint_enums(self) -> list[ThermostatSetpointType]:
|
||||
"""Return the list of enums that are relevant to the current thermostat mode."""
|
||||
if self._current_mode is None or self._current_mode.value is None:
|
||||
# Thermostat with no support for setting a mode is just a setpoint
|
||||
if self.info.primary_value.property_key is None:
|
||||
return []
|
||||
return [ThermostatSetpointType(int(self.info.primary_value.property_key))]
|
||||
|
||||
# Thermostat(valve) with no support for setting a mode
|
||||
# is considered heating-only
|
||||
return [ThermostatSetpointType.HEATING]
|
||||
return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), [])
|
||||
|
||||
@property
|
||||
|
||||
@@ -206,7 +206,7 @@ class ZWaveBaseEntity(Entity):
|
||||
):
|
||||
name += f" ({primary_value.endpoint})"
|
||||
|
||||
return name
|
||||
return name.strip()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -72,6 +72,8 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity):
|
||||
if self._attr_available_tones:
|
||||
self._attr_supported_features |= SirenEntityFeature.TONES
|
||||
|
||||
self._attr_name = self.generate_name(include_value_name=True)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return whether device is on."""
|
||||
|
||||
@@ -223,6 +223,7 @@ class ConfigEntry:
|
||||
"_async_cancel_retry_setup",
|
||||
"_on_unload",
|
||||
"reload_lock",
|
||||
"_reauth_lock",
|
||||
"_tasks",
|
||||
"_background_tasks",
|
||||
"_integration_for_domain",
|
||||
@@ -321,6 +322,8 @@ class ConfigEntry:
|
||||
|
||||
# Reload lock to prevent conflicting reloads
|
||||
self.reload_lock = asyncio.Lock()
|
||||
# Reauth lock to prevent concurrent reauth flows
|
||||
self._reauth_lock = asyncio.Lock()
|
||||
|
||||
self._tasks: set[asyncio.Future[Any]] = set()
|
||||
self._background_tasks: set[asyncio.Future[Any]] = set()
|
||||
@@ -727,12 +730,28 @@ class ConfigEntry:
|
||||
data: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Start a reauth flow."""
|
||||
# We will check this again in the task when we hold the lock,
|
||||
# but we also check it now to try to avoid creating the task.
|
||||
if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})):
|
||||
# Reauth flow already in progress for this entry
|
||||
return
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
self._async_init_reauth(hass, context, data),
|
||||
f"config entry reauth {self.title} {self.domain} {self.entry_id}",
|
||||
)
|
||||
|
||||
async def _async_init_reauth(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
context: dict[str, Any] | None = None,
|
||||
data: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Start a reauth flow."""
|
||||
async with self._reauth_lock:
|
||||
if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})):
|
||||
# Reauth flow already in progress for this entry
|
||||
return
|
||||
await hass.config_entries.flow.async_init(
|
||||
self.domain,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
@@ -742,9 +761,7 @@ class ConfigEntry:
|
||||
}
|
||||
| (context or {}),
|
||||
data=self.data | (data or {}),
|
||||
),
|
||||
f"config entry reauth {self.title} {self.domain} {self.entry_id}",
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_get_active_flows(
|
||||
@@ -754,7 +771,9 @@ class ConfigEntry:
|
||||
return (
|
||||
flow
|
||||
for flow in hass.config_entries.flow.async_progress_by_handler(
|
||||
self.domain, match_context={"entry_id": self.entry_id}
|
||||
self.domain,
|
||||
match_context={"entry_id": self.entry_id},
|
||||
include_uninitialized=True,
|
||||
)
|
||||
if flow["context"].get("source") in sources
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Final
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2023
|
||||
MINOR_VERSION: Final = 11
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
PATCH_VERSION: Final = "0"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
||||
|
||||
@@ -127,7 +127,7 @@ async def async_check_ha_config_file( # noqa: C901
|
||||
try:
|
||||
integration = await async_get_integration_with_requirements(hass, domain)
|
||||
except loader.IntegrationNotFound as ex:
|
||||
if not hass.config.recovery_mode:
|
||||
if not hass.config.recovery_mode and not hass.config.safe_mode:
|
||||
result.add_error(f"Integration error: {domain} - {ex}")
|
||||
continue
|
||||
except RequirementsNotFound as ex:
|
||||
@@ -216,7 +216,7 @@ async def async_check_ha_config_file( # noqa: C901
|
||||
)
|
||||
platform = p_integration.get_platform(domain)
|
||||
except loader.IntegrationNotFound as ex:
|
||||
if not hass.config.recovery_mode:
|
||||
if not hass.config.recovery_mode and not hass.config.safe_mode:
|
||||
result.add_error(f"Platform error {domain}.{p_name} - {ex}")
|
||||
continue
|
||||
except (
|
||||
|
||||
@@ -8,7 +8,7 @@ atomicwrites-homeassistant==1.4.1
|
||||
attrs==23.1.0
|
||||
awesomeversion==23.8.0
|
||||
bcrypt==4.0.1
|
||||
bleak-retry-connector==3.2.1
|
||||
bleak-retry-connector==3.3.0
|
||||
bleak==0.21.1
|
||||
bluetooth-adapters==0.16.1
|
||||
bluetooth-auto-recovery==1.2.3
|
||||
@@ -22,7 +22,7 @@ ha-av==10.1.1
|
||||
hass-nabucasa==0.74.0
|
||||
hassil==1.2.5
|
||||
home-assistant-bluetooth==1.10.4
|
||||
home-assistant-frontend==20231025.1
|
||||
home-assistant-frontend==20231030.1
|
||||
home-assistant-intents==2023.10.16
|
||||
httpx==0.25.0
|
||||
ifaddr==0.2.0
|
||||
|
||||
@@ -2469,7 +2469,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = {
|
||||
function_name="async_move_todo_item",
|
||||
arg_types={
|
||||
1: "str",
|
||||
2: "int",
|
||||
2: "str | None",
|
||||
},
|
||||
return_type="None",
|
||||
),
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2023.11.0.dev0"
|
||||
version = "2023.11.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -26,7 +26,7 @@ CO2Signal==0.4.2
|
||||
DoorBirdPy==2.1.0
|
||||
|
||||
# homeassistant.components.homekit
|
||||
HAP-python==4.9.0
|
||||
HAP-python==4.9.1
|
||||
|
||||
# homeassistant.components.tasmota
|
||||
HATasmota==0.7.3
|
||||
@@ -113,7 +113,7 @@ PyTransportNSW==0.1.1
|
||||
PyTurboJPEG==1.7.1
|
||||
|
||||
# homeassistant.components.vicare
|
||||
PyViCare==2.25.0
|
||||
PyViCare==2.28.1
|
||||
|
||||
# homeassistant.components.xiaomi_aqara
|
||||
PyXiaomiGateway==0.14.3
|
||||
@@ -192,7 +192,7 @@ aio-georss-gdacs==0.8
|
||||
aioairq==0.2.4
|
||||
|
||||
# homeassistant.components.airzone_cloud
|
||||
aioairzone-cloud==0.3.0
|
||||
aioairzone-cloud==0.3.1
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.6.9
|
||||
@@ -255,7 +255,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.10
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==3.0.8
|
||||
aiohomekit==3.0.9
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -375,7 +375,7 @@ aiounifi==64
|
||||
aiovlc==0.1.0
|
||||
|
||||
# homeassistant.components.vodafone_station
|
||||
aiovodafone==0.4.1
|
||||
aiovodafone==0.4.2
|
||||
|
||||
# homeassistant.components.waqi
|
||||
aiowaqi==2.1.0
|
||||
@@ -387,7 +387,7 @@ aiowatttime==0.1.1
|
||||
aiowebostv==0.3.3
|
||||
|
||||
# homeassistant.components.withings
|
||||
aiowithings==1.0.1
|
||||
aiowithings==1.0.2
|
||||
|
||||
# homeassistant.components.yandex_transport
|
||||
aioymaps==1.2.2
|
||||
@@ -414,7 +414,7 @@ amberelectric==1.0.4
|
||||
amcrest==1.9.8
|
||||
|
||||
# homeassistant.components.androidtv
|
||||
androidtv[async]==0.0.72
|
||||
androidtv[async]==0.0.73
|
||||
|
||||
# homeassistant.components.androidtv_remote
|
||||
androidtvremote2==0.0.14
|
||||
@@ -530,7 +530,7 @@ bimmer-connected==0.14.2
|
||||
bizkaibus==0.1.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==3.2.1
|
||||
bleak-retry-connector==3.3.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.21.1
|
||||
@@ -857,7 +857,7 @@ gassist-text==0.0.10
|
||||
gcal-sync==5.0.0
|
||||
|
||||
# homeassistant.components.geniushub
|
||||
geniushub-client==0.7.0
|
||||
geniushub-client==0.7.1
|
||||
|
||||
# homeassistant.components.geocaching
|
||||
geocachingapi==0.2.1
|
||||
@@ -910,7 +910,7 @@ google-cloud-texttospeech==2.12.3
|
||||
google-generativeai==0.1.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==3.0.2
|
||||
google-nest-sdm==3.0.3
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@@ -1007,7 +1007,7 @@ hole==0.8.0
|
||||
holidays==0.28
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20231025.1
|
||||
home-assistant-frontend==20231030.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2023.10.16
|
||||
@@ -1394,7 +1394,7 @@ openwrt-luci-rpc==1.1.16
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.0.37
|
||||
opower==0.0.38
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
@@ -1684,7 +1684,7 @@ pyebox==1.1.4
|
||||
pyecoforest==0.3.0
|
||||
|
||||
# homeassistant.components.econet
|
||||
pyeconet==0.1.20
|
||||
pyeconet==0.1.22
|
||||
|
||||
# homeassistant.components.edimax
|
||||
pyedimax==0.2.1
|
||||
@@ -2004,7 +2004,7 @@ pysabnzbd==1.1.1
|
||||
pysaj==0.0.16
|
||||
|
||||
# homeassistant.components.schlage
|
||||
pyschlage==2023.9.1
|
||||
pyschlage==2023.10.0
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.35
|
||||
@@ -2141,7 +2141,7 @@ python-join-api==0.0.9
|
||||
python-juicenet==1.1.0
|
||||
|
||||
# homeassistant.components.tplink
|
||||
python-kasa[speedups]==0.5.3
|
||||
python-kasa[speedups]==0.5.4
|
||||
|
||||
# homeassistant.components.lirc
|
||||
# python-lirc==1.2.3
|
||||
@@ -2319,7 +2319,7 @@ renault-api==0.2.0
|
||||
renson-endura-delta==1.6.0
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.7.11
|
||||
reolink-aio==0.7.12
|
||||
|
||||
# homeassistant.components.idteck_prox
|
||||
rfk101py==0.0.1
|
||||
@@ -2382,7 +2382,7 @@ satel-integra==0.3.7
|
||||
scapy==2.5.0
|
||||
|
||||
# homeassistant.components.screenlogic
|
||||
screenlogicpy==0.9.3
|
||||
screenlogicpy==0.9.4
|
||||
|
||||
# homeassistant.components.scsgate
|
||||
scsgate==0.1.0
|
||||
@@ -2488,7 +2488,7 @@ starline==0.1.5
|
||||
starlingbank==3.2
|
||||
|
||||
# homeassistant.components.starlink
|
||||
starlink-grpc-core==1.1.2
|
||||
starlink-grpc-core==1.1.3
|
||||
|
||||
# homeassistant.components.statsd
|
||||
statsd==3.2.1
|
||||
@@ -2512,7 +2512,7 @@ streamlabswater==1.0.1
|
||||
stringcase==1.2.0
|
||||
|
||||
# homeassistant.components.subaru
|
||||
subarulink==0.7.6
|
||||
subarulink==0.7.8
|
||||
|
||||
# homeassistant.components.solarlog
|
||||
sunwatcher==0.2.1
|
||||
@@ -2661,7 +2661,7 @@ vallox-websocket-api==3.3.0
|
||||
vehicle==2.0.0
|
||||
|
||||
# homeassistant.components.velbus
|
||||
velbus-aio==2023.10.1
|
||||
velbus-aio==2023.10.2
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.19
|
||||
@@ -2682,7 +2682,7 @@ volvooncall==0.10.3
|
||||
vsure==2.6.6
|
||||
|
||||
# homeassistant.components.vasttrafik
|
||||
vtjp==0.1.14
|
||||
vtjp==0.2.1
|
||||
|
||||
# homeassistant.components.vulcan
|
||||
vulcan-api==2.3.0
|
||||
@@ -2740,7 +2740,7 @@ xiaomi-ble==0.21.1
|
||||
xknx==2.11.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknxproject==3.3.0
|
||||
xknxproject==3.4.0
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
# homeassistant.components.fritz
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user