Compare commits

...

94 Commits

Author SHA1 Message Date
Franck Nijhof
0b8d4235c3 2023.11.0 (#103164) 2023-11-01 15:59:51 +01:00
Franck Nijhof
4ce859b4e4 Bumped version to 2023.11.0 2023-11-01 11:24:41 +01:00
Franck Nijhof
18acec32b8 Bumped version to 2023.11.0b6 2023-11-01 11:22:25 +01:00
Bram Kragten
cfa2f2ce61 Update frontend to 20231030.1 (#103163) 2023-11-01 11:22:16 +01:00
Jan Bouwhuis
aa5ea5ebc3 Fix mqtt is not reloading without yaml config (#103159) 2023-11-01 11:22:13 +01:00
J. Nick Koston
bcea021c14 Allow non-admins to subscribe to the issue registry updated event (#103145) 2023-11-01 11:22:10 +01:00
Allen Porter
ea2d2ba7b7 Improve fitbit oauth token error handling in config flow (#103131)
* Improve fitbit oauth token error handling in config flow

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests with updated error reason

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-11-01 11:22:07 +01:00
Teemu R
c5f21fefbe Bump python-kasa to 0.5.4 for tplink (#103038) 2023-11-01 11:22:03 +01:00
Franck Nijhof
9910f9e0ae Bumped version to 2023.11.0b5 2023-10-31 19:43:21 +01:00
J. Nick Koston
f0a06efa1f Fix race in starting reauth flows (#103130) 2023-10-31 19:43:05 +01:00
J. Nick Koston
8992d15ffc Bump aiohomekit to 3.0.9 (#103123) 2023-10-31 19:43:02 +01:00
Paul Bottein
e097dc02dd Don't try to load resources in safe mode (#103122) 2023-10-31 19:42:59 +01:00
starkillerOG
bfae1468d6 Bump reolink-aio to 0.7.12 (#103120) 2023-10-31 19:42:52 +01:00
Christopher Fenner
09ed6e9f9b Handle exception introduced with recent PyViCare update (#103110) 2023-10-31 19:42:48 +01:00
Erik Montnemery
040ecb74e0 Add todo to core files (#103102) 2023-10-31 19:42:45 +01:00
Erik Montnemery
a48e63aa28 Fix todoist todo tests (#103101) 2023-10-31 19:42:41 +01:00
Erik Montnemery
19479b2a68 Fix local_todo todo tests (#103099) 2023-10-31 19:42:36 +01:00
Franck Nijhof
9ae29e243d Bumped version to 2023.11.0b4 2023-10-31 13:30:10 +01:00
Joost Lekkerkerker
e309bd764b Abort config flow if Google Tasks API is not enabled (#103114)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-10-31 13:29:52 +01:00
Christopher Fenner
777ffe6946 Fix client id label in ViCare integration (#103111) 2023-10-31 13:29:49 +01:00
Robert Resch
fa0f679a9a Fix todo.remove_item frontend (#103108) 2023-10-31 13:29:46 +01:00
Erik Montnemery
26b7e94c4f Fix shopping_list todo tests (#103100) 2023-10-31 13:29:43 +01:00
Erik Montnemery
957998ea8d Fix google_tasks todo tests (#103098) 2023-10-31 13:29:40 +01:00
Erik Montnemery
abaeacbd6b Fix restore state for light when saved attribute is None (#103096) 2023-10-31 13:29:36 +01:00
Bram Kragten
d76c16fa3a Update frontend to 20231030.0 (#103086) 2023-10-31 13:29:33 +01:00
G Johansson
67edb98e59 Fix Met Device Info (#103082) 2023-10-31 13:29:30 +01:00
Robert Resch
376a79eb42 Refactor todo services and their schema (#103079) 2023-10-31 13:29:27 +01:00
Rami Mosleh
41500cbe9b Code cleanup for transmission integration (#103078) 2023-10-31 13:29:24 +01:00
Paul Manzotti
06f27e7e74 Update geniushub-client to v0.7.1 (#103071) 2023-10-31 13:29:19 +01:00
Franck Nijhof
a3ebfaebe7 Bumped version to 2023.11.0b3 2023-10-30 19:59:32 +01:00
Joost Lekkerkerker
8d781ff063 Add 2 properties to Withings diagnostics (#103067) 2023-10-30 19:59:21 +01:00
Joost Lekkerkerker
bac39f0061 Show a warning when no Withings data found (#103066) 2023-10-30 19:59:17 +01:00
David Knowles
c7b702f3c2 Bump pyschlage to 2023.10.0 (#103065) 2023-10-30 19:59:14 +01:00
Christopher Fenner
3728f3da69 Update PyViCare to v2.28.1 for ViCare integration (#103064) 2023-10-30 19:59:11 +01:00
tronikos
31d8f4b35d Fix Opower not refreshing statistics when there are no forecast entities (#103058)
Ensure _insert_statistics is periodically called
2023-10-30 19:59:08 +01:00
Mike Woudenberg
f113d9aa71 Use correct config entry field to update when IP changes in loqed (#103051) 2023-10-30 19:59:05 +01:00
Jack Boswell
891ad0b1be Bump starlink-grpc-core to 1.1.3 (#103043) 2023-10-30 19:59:02 +01:00
Jirka
5c16a8247a Update MQTT QoS description string (#103036)
Update strings.json
2023-10-30 19:58:58 +01:00
Allen Porter
483671bf9f Bump google-nest-sdm to 3.0.3 (#103035) 2023-10-30 19:58:54 +01:00
G-Two
6f73d2aac5 Bump to subarulink 0.7.8 (#103033) 2023-10-30 19:58:50 +01:00
Allen Porter
f5b3661836 Fix bug in fitbit credential import for expired tokens (#103024)
* Fix bug in fitbit credential import on token refresh

* Use stable test ids

* Update homeassistant/components/fitbit/sensor.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-10-30 19:58:47 +01:00
kpine
f70c13214c Revert "Fix temperature setting for multi-setpoint z-wave device (#102395)" (#103022)
This reverts commit 2d6dc2bccc.
2023-10-30 19:58:43 +01:00
Raman Gupta
70e8978123 Fix zwave_js siren name (#103016)
* Fix zwave_js.siren name

* Fix test
2023-10-30 19:58:40 +01:00
Diogo Gomes
031b1c26ce Fix utility_meter reset when DST change occurs (#103012) 2023-10-30 19:58:37 +01:00
Nortonko
13580a334f Bump python-androidtv to 0.0.73 (#102999)
* Update manifest.json

Bump python-androidtv to version 0.0.73

* bump androidtv 0.0.73

* bump androidtv 0.0.73
2023-10-30 19:58:34 +01:00
Michael
e81bfb959e Fix proximity entity id (#102992)
* fix proximity entity id

* extend test to cover entity id
2023-10-30 19:58:31 +01:00
Tom Puttemans
fefe930506 DSMR Gas currently delivered device state class conflict (#102991)
Fixes #102985
2023-10-30 19:58:28 +01:00
David Bonnes
5ac7e8b1ac Harden evohome against failures to retrieve high-precision temps (#102989)
fix hass-logger-period
2023-10-30 19:58:24 +01:00
tronikos
36512f7157 Bump opower to 0.0.38 (#102983) 2023-10-30 19:58:21 +01:00
Bouwe Westerdijk
cc3ae9e103 Correct total state_class of huisbaasje sensors (#102945)
* Change all cumulative-interval sensors to TOTAL
2023-10-30 19:58:18 +01:00
Robert Hillis
12482216f6 Fix Google Mail expired authorization (#102735)
* Fix Google Mail expired authorization

* add test

* raise HomeAssistantError

* handle in api module

* uno mas
2023-10-30 19:58:14 +01:00
David Knowles
20409d0124 Make Hydrawise initialize data immediately (#101936) 2023-10-30 19:58:11 +01:00
mkmer
a741bc9951 Add retry before unavailable to Honeywell (#101702)
Co-authored-by: Robert Resch <robert@resch.dev>
2023-10-30 19:58:08 +01:00
Erwin Douna
59d2bce369 Enable dry mode for Tado AC's V3 (#99568) 2023-10-30 19:58:04 +01:00
Paulus Schoutsen
eef318f63c Bumped version to 2023.11.0b2 2023-10-28 23:29:03 -04:00
Paulus Schoutsen
9c8a4bb4eb Fix proximity zone handling (#102971)
* fix proximity zone

* fix test
2023-10-28 23:29:03 -04:00
Paulus Schoutsen
9c9f1ea685 Fix error message strings for Todoist configuration flow (#102968)
* Fix error message strings for Todoist configuration flow

* Update error code in test
2023-10-28 23:29:03 -04:00
Paulus Schoutsen
85d999b020 Add gas device class to dsmr_reader sensor (#102953)
DSMR reader integration - can't configure gas meter in energy dashboard posible due to missing device_class
Fixes #102367
2023-10-28 23:29:03 -04:00
Paulus Schoutsen
bcddf52364 Update xknxproject to 3.4.0 (#102946) 2023-10-28 23:29:03 -04:00
Paulus Schoutsen
07e4e1379a Improve diagnostic handling in HomeWizard Energy (#102935) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
f9f010643a Handle/extend number entity availability property in HomeWizard Energy (#102934) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
974c34e2b6 Small base entity cleanup for HomeWizard Energy entities (#102933) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
1c3de76b04 Move HomeWizard Energy identify button to config entity category (#102932) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
bee63ca654 Hide mac address from HomeWizard Energy config entry/discovery titles (#102931) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
29c99f419f Bump velbusaio to 2023.10.2 (#102919) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
3d321c5ca7 Update frontend to 20231027.0 (#102913) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
4617c16a96 Update aioairzone-cloud to v0.3.1 (#102899) 2023-10-28 23:29:02 -04:00
Paulus Schoutsen
a60656bf29 Improve fitbit oauth import robustness (#102833)
* Improve fitbit oauth import robustness

* Improve sensor tests and remove unnecessary client check

* Fix oauth client id/secret config key checks

* Add executor for sync call
2023-10-28 23:29:02 -04:00
Paulus Schoutsen
2eb2a65197 Use new API for Vasttrafik (#102570) 2023-10-28 23:29:02 -04:00
Franck Nijhof
867aaf10ee Bumped version to 2023.11.0b1 2023-10-27 14:02:42 +02:00
Franck Nijhof
7fe1ac901f Some textual fixes for todo (#102895) 2023-10-27 14:02:24 +02:00
Bram Kragten
5dca3844ef Add redirect from shopping list to todo (#102894) 2023-10-27 14:02:20 +02:00
Erik Montnemery
b5c75a2f2f Allow missing components in safe mode (#102891) 2023-10-27 14:02:17 +02:00
Erik Montnemery
62fc9dfd6c Allow missing components in safe mode (#102888) 2023-10-27 14:02:14 +02:00
Jan Bouwhuis
0573981d6f Fix mqtt schema import not available for mqtt_room (#102866) 2023-10-27 14:02:09 +02:00
Paul Bottein
cc7a4d01e3 Don't return resources in safe mode (#102865) 2023-10-27 14:02:06 +02:00
Paul Bottein
293025ab6c Update frontend to 20231026.0 (#102857) 2023-10-27 14:02:02 +02:00
Jan-Philipp Benecke
a490b5e286 Add connections to PassiveBluetoothProcessorEntity (#102854) 2023-10-27 14:01:58 +02:00
Joost Lekkerkerker
7e4da1d03b Bump aiowithings to 1.0.2 (#102852) 2023-10-27 14:01:53 +02:00
Ravaka Razafimanantsoa
9e140864eb Address late review of switchbot cloud (#102842)
For Martin's review
2023-10-27 14:01:47 +02:00
Kevin Worrel
a6f88fb123 Bump screenlogicpy to v0.9.4 (#102836) 2023-10-27 14:01:43 +02:00
J. Nick Koston
386c5ecc3e Bump bleak-retry-connector to 3.3.0 (#102825)
changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v3.2.1...v3.3.0
2023-10-27 14:01:39 +02:00
Erik Montnemery
0d7fb5b026 Use real devices in automation blueprint tests (#102824) 2023-10-27 14:01:35 +02:00
Erik Montnemery
767b7ba4d6 Correct logic for picking bluetooth local name (#102823)
* Correct logic for picking bluetooth local name

* make test more robust

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2023-10-27 14:01:31 +02:00
William Scanlon
f2cef7245a Bump pyeconet to 0.1.22 to handle breaking API change (#102820) 2023-10-27 14:01:27 +02:00
J. Nick Koston
701a5d7758 Bump HAP-python 4.9.1 (#102811) 2023-10-27 14:01:23 +02:00
mkmer
244fccdae6 Move coordinator first refresh in Blink (#102805)
Move coordinator first refresh
2023-10-27 14:01:18 +02:00
Erik Montnemery
10e6a26717 Fix fan device actions (#102797) 2023-10-27 14:01:15 +02:00
Allen Porter
5fe5013198 Change todo move API to reference previous uid (#102795) 2023-10-27 14:01:11 +02:00
Marc Mueller
0a0584b053 Fix velbus import (#102780) 2023-10-27 14:01:07 +02:00
Erik Montnemery
62733e830f Improve validation of device automations (#102766)
* Improve validation of device automations

* Improve comments

* Address review comment
2023-10-27 14:01:02 +02:00
Simone Chemelli
bbcfb5f30e Improve exception handling for Vodafone Station (#102761)
* improve exception handling for Vodafone Station

* address review comment

* apply review comment

* better except handling (bump library)

* cleanup
2023-10-27 14:00:55 +02:00
Amit Finkelstein
5b0e0b07b3 Apple TV: Use replacement commands for deprecated ones (#102056)
Co-authored-by: Robert Resch <robert@resch.dev>
2023-10-27 14:00:45 +02:00
Franck Nijhof
05fd64fe80 Bumped version to 2023.11.0b0 2023-10-25 17:41:53 +02:00
147 changed files with 1795 additions and 5625 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,3 +10,4 @@ DEFAULT_HEAT_AWAY_TEMPERATURE = 61
CONF_DEV_ID = "thermostat"
CONF_LOC_ID = "location"
_LOGGER = logging.getLogger(__name__)
RETRY = 3

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.7.11"]
"requirements": ["reolink-aio==0.7.12"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,9 +30,7 @@
"options": {
"step": {
"init": {
"title": "Configure options for Transmission",
"data": {
"scan_interval": "Update frequency",
"limit": "Limit",
"order": "Order"
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@
"velbus-packet",
"velbus-protocol"
],
"requirements": ["velbus-aio==2023.10.1"],
"requirements": ["velbus-aio==2023.10.2"],
"usb": [
{
"vid": "10CF",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_push",
"loggers": ["aiowithings"],
"quality_scale": "platinum",
"requirements": ["aiowithings==1.0.1"]
"requirements": ["aiowithings==1.0.2"]
}

View File

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

View File

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

View File

@@ -206,7 +206,7 @@ class ZWaveBaseEntity(Entity):
):
name += f" ({primary_value.endpoint})"
return name
return name.strip()
@property
def available(self) -> bool:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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