Compare commits

...

83 Commits

Author SHA1 Message Date
Paulus Schoutsen
ba237fd383 Bumped version to 2022.2.0b6 2022-02-01 10:00:39 -08:00
Paulus Schoutsen
b687f68d53 Bump frontend to 20220201.0 (#65380) 2022-02-01 10:00:36 -08:00
Robert Svensson
f3c39d8dca Redact host address in UniFi diagnostics (#65379) 2022-02-01 09:59:25 -08:00
Michael
19fff6489b Fix wan_access switch for disconnected devices in Fritz!Tools (#65378) 2022-02-01 09:59:24 -08:00
Bram Kragten
4f8752b351 Allow removing keys from automation (#65374) 2022-02-01 09:59:23 -08:00
G Johansson
03bd3f5001 Fix options for dnsip (#65369) 2022-02-01 09:59:22 -08:00
Erik Montnemery
055382c84c Improve CastProtocol (#65357)
* Improve CastProtocol

* Tweak
2022-02-01 09:59:21 -08:00
Paulus Schoutsen
68651be2cc Simplify unifi cleanup logic (#65345) 2022-02-01 09:59:21 -08:00
schreyack
09c2c129b9 Fix honeywell hold mode (#65327)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-02-01 09:59:20 -08:00
ZuluWhiskey
7fe1b85495 Fix MotionEye config flow (#64360)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-02-01 09:59:19 -08:00
Paulus Schoutsen
5082582769 Bumped version to 2022.2.0b5 2022-01-31 17:12:40 -08:00
Paulus Schoutsen
b7c7571a39 I zone, you zone, we zoning (#65344) 2022-01-31 17:12:26 -08:00
Simone Chemelli
63a90b7226 Add diagnostics for SamsungTV (#65342) 2022-01-31 17:12:25 -08:00
J. Nick Koston
5735762af2 Bump zeroconf to 0.38.3 (#65341) 2022-01-31 17:12:25 -08:00
Paulus Schoutsen
90127d04fa Bump aiohue to 4.0.1 (#65340) 2022-01-31 17:12:24 -08:00
Paulus Schoutsen
114da0bd4f Bump version tag on async_timeout warning (#65339) 2022-01-31 17:12:23 -08:00
J. Nick Koston
5c3d4cb9a5 Prevent unifiprotect from being rediscovered on UDM-PROs (#65335) 2022-01-31 17:12:23 -08:00
Simone Chemelli
3f8d2f3102 Add diagnostics support to Fritz (#65334)
* Add diagnostics support to Fritz

* Temporary remove tests

* coveragerc
2022-01-31 17:12:22 -08:00
J. Nick Koston
eea9e26ef5 Fix guardian being rediscovered via dhcp (#65332) 2022-01-31 17:12:21 -08:00
Michael
649b4ce329 Improve debugging and error handling in Fritz!Tools (#65324) 2022-01-31 17:12:21 -08:00
Simone Chemelli
1facd0edd4 Fritz tests cleanup (#65054) 2022-01-31 17:12:20 -08:00
Paulus Schoutsen
1fbd624a24 Alexa to handle brightness and catch exceptions (#65322) 2022-01-31 17:10:25 -08:00
Pascal Winters
0a000babc9 Bump pyps4-2ndscreen to 1.3.1 (#65320) 2022-01-31 17:10:25 -08:00
Franck Nijhof
74632d26fa Ensure PVOutput connection error is logged (#65319) 2022-01-31 17:10:24 -08:00
Franck Nijhof
87b20c6abe Update tailscale to 0.2.0 (#65318) 2022-01-31 17:10:23 -08:00
Franck Nijhof
ea511357b6 Add diagnostics support to WLED (#65317) 2022-01-31 17:10:23 -08:00
Jeff Irion
00b2c85e98 Bump androidtv to 0.0.61 (#65315) 2022-01-31 17:10:22 -08:00
J. Nick Koston
961cf15e6e Improve reliability of august setup with recent api changes (#65314) 2022-01-31 17:10:21 -08:00
Franck Nijhof
7117395489 Fix missing expiration data in Whois information (#65313) 2022-01-31 17:10:21 -08:00
Franck Nijhof
5dc92bb2ce Update wled to 0.13.0 (#65312) 2022-01-31 17:10:19 -08:00
Franck Nijhof
0519b29501 Update adguard to 0.5.1 (#65305) 2022-01-31 17:10:19 -08:00
Duco Sebel
4f8e19ed4a Add HomeWizard diagnostics (#65297)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-01-31 17:10:18 -08:00
Duco Sebel
fd7f66fbdc Fix HomeWizard unclosed clientsession error when closing Home Assistant (#65296) 2022-01-31 17:10:17 -08:00
Mick Vleeshouwer
9294319048 Bump pyoverkiz to 1.3.2 (#65293) 2022-01-31 17:09:29 -08:00
starkillerOG
c1019394ed Bump pynetgear to 0.9.1 (#65290) 2022-01-31 17:06:23 -08:00
Erik Montnemery
0885d48186 Correct cast media browse filter for audio groups (#65288) 2022-01-31 17:06:22 -08:00
Tobias Sauerwein
13ad1cc56c Bump pyatmo to v.6.2.4 (#65285)
* Bump pyatmo to v6.2.3

Signed-off-by: cgtobi <cgtobi@gmail.com>

* Bump pyatmo to v6.2.4

Signed-off-by: cgtobi <cgtobi@gmail.com>
2022-01-31 17:06:21 -08:00
J. Nick Koston
c5d68f8669 Increase august timeout and make failure to sync at startup non-fatal (#65281) 2022-01-31 17:06:21 -08:00
fOmey
2757976a5a Tuya fan percentage fix (#65225) 2022-01-31 17:06:20 -08:00
Erik Montnemery
73750d8a25 Add cast platform for extending Google Cast media_player (#65149)
* Add cast platform for extending Google Cast media_player

* Update tests

* Refactor according to review comments

* Add test for playing using a cast platform

* Apply suggestions from code review

Co-authored-by: jjlawren <jjlawren@users.noreply.github.com>

* Pass cast type instead of a filter function when browsing

* Raise on invalid cast platform

* Test media browsing

Co-authored-by: jjlawren <jjlawren@users.noreply.github.com>
2022-01-31 17:06:19 -08:00
jjlawren
2eef05eb84 Send notification to alert of Sonos networking issues (#65084)
* Send notification to alert of Sonos networking issues

* Add links to documentation
2022-01-31 17:06:19 -08:00
Brett Adams
3446c95cd3 Add diagnostics to Advantage Air (#65006)
Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2022-01-31 17:06:18 -08:00
Teemu R
cdcbb87d97 Bump python-kasa to 0.4.1 for tplink integration (#64123)
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-01-31 17:06:17 -08:00
Paulus Schoutsen
ef143b5eb2 Bumped version to 2022.2.0b4 2022-01-30 20:28:42 -08:00
Matthias Alphart
5d7aefa0b4 Update xknx to 0.19.1 (#65275) 2022-01-30 20:28:34 -08:00
Brynley McDonald
6b6bd381fd Fix flick_electric auth failures (#65274) 2022-01-30 20:28:34 -08:00
Shay Levy
252f5f6b35 Bump aiowebostv to 0.1.2 (#65267) 2022-01-30 20:28:33 -08:00
J. Nick Koston
8bdee9cb1c Simplify whois value_fn (#65265) 2022-01-30 20:28:32 -08:00
J. Nick Koston
7e350b8347 Handle missing attrs in whois results (#65254)
* Handle missing attrs in whois results

- Some attrs are not set depending on where the
  domain is registered

- Fixes #65164

* Set to unknown instead of do not create

* no multi-line lambda
2022-01-30 20:28:32 -08:00
J. Nick Koston
ac8a1248f9 Fix debugpy blocking the event loop at startup (#65252) 2022-01-30 20:28:31 -08:00
J. Nick Koston
ffe262abce Fix flux_led not generating unique ids when discovery fails (#65250) 2022-01-30 20:28:30 -08:00
J. Nick Koston
5174e68b16 Fix powerwall login retry when hitting rate limit (#65245) 2022-01-30 20:28:30 -08:00
Shay Levy
6e4c281e15 Fix webostv live TV source missing when configuring sources (#65243) 2022-01-30 20:28:29 -08:00
Joakim Sørensen
8e71e2e8ee Use .json.txt for diagnostics download filetype (#65236) 2022-01-30 20:28:28 -08:00
J. Nick Koston
26905115c8 Increase the timeout for flux_led directed discovery (#65222) 2022-01-30 20:28:28 -08:00
J. Nick Koston
eca3514f9e Fix senseme fan lights (#65217) 2022-01-30 20:28:27 -08:00
jjlawren
305ffc4ab6 Add activity statistics to Sonos diagnostics (#65214) 2022-01-30 20:28:26 -08:00
Robert Svensson
508fd0cb2a Add logic to avoid creating the same scene multiple times (#65207) 2022-01-30 20:28:25 -08:00
Shay Levy
5368fb6d54 Fix webostv configure sources when selected source is missing (#65195)
* Fix webostv configure sources when selected source is missing

* Add comment for filtering duplicates
2022-01-30 20:28:25 -08:00
Michael
d6527953c3 Fix "internet access" switch for Fritz connected device without known IP address (#65190)
* fix get wan access

* small improvement
- default wan_access to None
- test if dev_info.ip_address is not empty
2022-01-30 20:28:24 -08:00
Robert Svensson
14c969ef6d Better manage of nested lists (#65176) 2022-01-30 20:28:23 -08:00
Aaron Bach
f6f25fa4ff Add diagnostics to SimpliSafe (#65171)
* Add diagnostics to SimpliSafe

* Bump

* Cleanup
2022-01-30 20:28:23 -08:00
Aaron Bach
dcf6e61d4f Ensure diagnostics redaction can handle lists of lists (#65170)
* Ensure diagnostics redaction can handle lists of lists

* Code review

* Update homeassistant/components/diagnostics/util.py

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

* Code review

* Typing

* Revert "Typing"

This reverts commit 8a57f772caa5180b609175591d81dfc473769f70.

* New typing attempt

* Revert "New typing attempt"

This reverts commit e26e4aae69f62325fdd6af4d80c8fd1f74846e54.

* Fix typing

* Fix typing again

* Add tests

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2022-01-30 20:28:22 -08:00
Aaron Bach
2041d4c118 Clean up SimpliSafe config flow tests (#65167)
* Clean up SimpliSafe config flow tests

* Cleanup
2022-01-30 20:28:21 -08:00
starkillerOG
b40bcecac0 Aqara restore door sensor state on start (#65128)
* restore door sensor state on start

* fix import

* fix issues

* also fix Natgas, WaterLeak and Smoke sensors

* remove unnesesary async_schedule_update_ha_state
2022-01-30 20:28:21 -08:00
Erik Montnemery
2ed20df906 Minor refactoring of cast media_player (#65125) 2022-01-30 20:28:20 -08:00
Marvin Wichmann
1a6964448c Fix KNX Expose for strings longer than 14 bytes (#63026)
* Fix KNX Expose for too long strings

* Fix tests

* Catch exception and avoid error during config entry setup for exposures

* Properly catch exceptions in knx expose

* Fix pylint

* Fix CI

* Add test for conversion error
2022-01-30 20:28:19 -08:00
Marvin Wichmann
3dde12f887 Add tests for KNX diagnostic and expose (#64938)
* Add test for KNX diagnostic

* Add test for KNX expose

* Apply review suggestions
2022-01-30 20:27:37 -08:00
Paulus Schoutsen
cd6c182c07 Bumped version to 2022.2.0b3 2022-01-28 21:53:21 -08:00
J. Nick Koston
f8e0c41e91 Fix uncaught exception during isy994 dhcp discovery with ignored entry (#65165) 2022-01-28 21:53:12 -08:00
J. Nick Koston
5f56107116 Add additional blink OUIs to DHCP discovery (#65162) 2022-01-28 21:53:11 -08:00
J. Nick Koston
fb3c99a891 Add additional roomba OUIs to DHCP discovery (#65161) 2022-01-28 21:53:11 -08:00
J. Nick Koston
ca505b79b5 Add dhcp discovery to oncue (#65160) 2022-01-28 21:53:10 -08:00
J. Nick Koston
c74a8bf65a Add OUI for KL430 tplink light strip to discovery (#65159) 2022-01-28 21:53:09 -08:00
Franck Nijhof
406801ef73 Fix setting speed of Tuya fan (#65155) 2022-01-28 21:53:09 -08:00
Marc Mueller
2bfedcbdc5 Move remaining keys to setup.cfg (#65154)
* Move metadata keys

* Move options

* Delete setup.py

* Remove unused constants
* Remove deprecated test_suite key

* Improve metadata

* Only include homeassistant*, not script*
* Add long_desc_content_type
* Remove license file (auto-included by setuptools + wheels)

* Add setup.py

Pip 21.2 doesn't support editable installs without it.
2022-01-28 21:53:08 -08:00
Simone Chemelli
84f817eb25 Fix status for Fritz device tracker (#65152) 2022-01-28 21:53:07 -08:00
Simone Chemelli
4ead2f2f7e Fix excepton for SamsungTV getting device info (#65151) 2022-01-28 21:53:07 -08:00
Marc Mueller
421f9716a7 Use isolated build environments (#65145) 2022-01-28 21:53:06 -08:00
Allen Porter
25e6d8858c Update nest diagnostics (#65141) 2022-01-28 21:53:05 -08:00
Marc Mueller
3829a81d15 Move project_urls to setup.cfg (#65129) 2022-01-28 21:53:05 -08:00
Marc Mueller
9318843867 Move version metadata key to setup.cfg (#65091)
* Move version to setup.cfg
* Move python_requires to setup.cfg
* Add script to validate project metadata
* Add dedicated pre-commit hook
2022-01-28 21:53:04 -08:00
Marc Mueller
4eb787b619 Move install_requires to setup.cfg (#65095) 2022-01-28 21:52:33 -08:00
164 changed files with 4094 additions and 1352 deletions

View File

@@ -27,6 +27,7 @@ omit =
homeassistant/components/adguard/sensor.py
homeassistant/components/adguard/switch.py
homeassistant/components/ads/*
homeassistant/components/advantage_air/diagnostics.py
homeassistant/components/aemet/weather_update_coordinator.py
homeassistant/components/aftership/*
homeassistant/components/agent_dvr/alarm_control_panel.py
@@ -375,6 +376,7 @@ omit =
homeassistant/components/fritz/common.py
homeassistant/components/fritz/const.py
homeassistant/components/fritz/device_tracker.py
homeassistant/components/fritz/diagnostics.py
homeassistant/components/fritz/sensor.py
homeassistant/components/fritz/services.py
homeassistant/components/fritz/switch.py
@@ -462,6 +464,7 @@ omit =
homeassistant/components/homematic/*
homeassistant/components/home_plus_control/api.py
homeassistant/components/home_plus_control/switch.py
homeassistant/components/homewizard/diagnostics.py
homeassistant/components/homeworks/*
homeassistant/components/honeywell/__init__.py
homeassistant/components/honeywell/climate.py
@@ -560,12 +563,7 @@ omit =
homeassistant/components/knx/__init__.py
homeassistant/components/knx/climate.py
homeassistant/components/knx/cover.py
homeassistant/components/knx/diagnostics.py
homeassistant/components/knx/expose.py
homeassistant/components/knx/knx_entity.py
homeassistant/components/knx/light.py
homeassistant/components/knx/notify.py
homeassistant/components/knx/schema.py
homeassistant/components/kodi/__init__.py
homeassistant/components/kodi/browse_media.py
homeassistant/components/kodi/const.py
@@ -949,6 +947,7 @@ omit =
homeassistant/components/sabnzbd/*
homeassistant/components/saj/sensor.py
homeassistant/components/samsungtv/bridge.py
homeassistant/components/samsungtv/diagnostics.py
homeassistant/components/satel_integra/*
homeassistant/components/schluter/*
homeassistant/components/scrape/sensor.py

View File

@@ -76,8 +76,10 @@ jobs:
- name: Build package
shell: bash
run: |
pip install twine wheel
python setup.py sdist bdist_wheel
# Remove dist, build, and homeassistant.egg-info
# when build locally for testing!
pip install twine build
python -m build
- name: Upload package
shell: bash

View File

@@ -107,7 +107,7 @@ repos:
pass_filenames: false
language: script
types: [text]
files: ^(homeassistant/.+/manifest\.json|setup\.py|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$
files: ^(homeassistant/.+/manifest\.json|setup\.cfg|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$
- id: hassfest
name: hassfest
entry: script/run-in-env.sh python3 -m script.hassfest
@@ -115,3 +115,10 @@ repos:
language: script
types: [text]
files: ^(homeassistant/.+/(manifest|strings)\.json|\.coveragerc|\.strict-typing|homeassistant/.+/services\.yaml|script/hassfest/.+\.py)$
- id: hassfest-metadata
name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata
pass_filenames: false
language: script
types: [text]
files: ^(script/hassfest/.+\.py|homeassistant/const\.py$|setup\.cfg)$

View File

@@ -1,4 +1,3 @@
include README.rst
include LICENSE.md
graft homeassistant
recursive-exclude * *.py[co]

View File

@@ -17,7 +17,7 @@ def timeout(
loop = asyncio.get_running_loop()
else:
report(
"called async_timeout.timeout with loop keyword argument. The loop keyword argument is deprecated and calls will fail after Home Assistant 2022.2",
"called async_timeout.timeout with loop keyword argument. The loop keyword argument is deprecated and calls will fail after Home Assistant 2022.3",
error_if_core=False,
)
if delay is not None:
@@ -30,7 +30,7 @@ def timeout(
def current_task(loop: asyncio.AbstractEventLoop) -> asyncio.Task[Any] | None:
"""Backwards compatible current_task."""
report(
"called async_timeout.current_task. The current_task call is deprecated and calls will fail after Home Assistant 2022.2; use asyncio.current_task instead",
"called async_timeout.current_task. The current_task call is deprecated and calls will fail after Home Assistant 2022.3; use asyncio.current_task instead",
error_if_core=False,
)
return asyncio.current_task()

View File

@@ -80,8 +80,8 @@ class AdGuardHomeFlowHandler(ConfigFlow, domain=DOMAIN):
adguard = AdGuardHome(
user_input[CONF_HOST],
port=user_input[CONF_PORT],
username=username, # type:ignore[arg-type]
password=password, # type:ignore[arg-type]
username=username,
password=password,
tls=user_input[CONF_SSL],
verify_ssl=user_input[CONF_VERIFY_SSL],
session=session,

View File

@@ -3,7 +3,7 @@
"name": "AdGuard Home",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/adguard",
"requirements": ["adguardhome==0.5.0"],
"requirements": ["adguardhome==0.5.1"],
"codeowners": ["@frenck"],
"iot_class": "local_polling"
}

View File

@@ -0,0 +1,25 @@
"""Provides diagnostics for Advantage Air."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
TO_REDACT = ["dealerPhoneNumber", "latitude", "logoPIN", "longitude", "postCode"]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id]["coordinator"].data
# Return only the relevant children
return {
"aircons": data["aircons"],
"system": async_redact_data(data["system"], TO_REDACT),
}

View File

@@ -1,6 +1,8 @@
"""Alexa related errors."""
from __future__ import annotations
from typing import Literal
from homeassistant.exceptions import HomeAssistantError
from .const import API_TEMP_UNITS
@@ -58,6 +60,30 @@ class AlexaInvalidValueError(AlexaError):
error_type = "INVALID_VALUE"
class AlexaInteralError(AlexaError):
"""Class to represent internal errors."""
namespace = "Alexa"
error_type = "INTERNAL_ERROR"
class AlexaNotSupportedInCurrentMode(AlexaError):
"""The device is not in the correct mode to support this command."""
namespace = "Alexa"
error_type = "NOT_SUPPORTED_IN_CURRENT_MODE"
def __init__(
self,
endpoint_id: str,
current_mode: Literal["COLOR", "ASLEEP", "NOT_PROVISIONED", "OTHER"],
) -> None:
"""Initialize invalid endpoint error."""
msg = f"Not supported while in {current_mode} mode"
AlexaError.__init__(self, msg, {"currentDeviceMode": current_mode})
self.endpoint_id = endpoint_id
class AlexaUnsupportedThermostatModeError(AlexaError):
"""Class to represent UnsupportedThermostatMode errors."""

View File

@@ -212,20 +212,14 @@ async def async_api_adjust_brightness(hass, config, directive, context):
entity = directive.entity
brightness_delta = int(directive.payload["brightnessDelta"])
# read current state
try:
current = math.floor(
int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100
)
except ZeroDivisionError:
current = 0
# set brightness
brightness = max(0, brightness_delta + current)
await hass.services.async_call(
entity.domain,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness},
{
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS_STEP_PCT: brightness_delta,
},
blocking=False,
context=context,
)

View File

@@ -48,8 +48,18 @@ async def async_handle_message(hass, config, request, context=None, enabled=True
response = directive.error()
except AlexaError as err:
response = directive.error(
error_type=err.error_type, error_message=err.error_message
error_type=err.error_type,
error_message=err.error_message,
payload=err.payload,
)
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
"Uncaught exception processing Alexa %s/%s request (%s)",
directive.namespace,
directive.name,
directive.entity_id or "-",
)
response = directive.error(error_message="Unknown error")
request_info = {"namespace": directive.namespace, "name": directive.name}

View File

@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/androidtv",
"requirements": [
"adb-shell[async]==0.4.0",
"androidtv[async]==0.0.60",
"androidtv[async]==0.0.61",
"pure-python-adb[async]==0.3.0.dev0"
],
"codeowners": ["@JeffLIrion", "@ollo69"],

View File

@@ -43,7 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return await async_setup_august(hass, entry, august_gateway)
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except (ClientResponseError, CannotConnect, asyncio.TimeoutError) as err:
except asyncio.TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to august api") from err
except (ClientResponseError, CannotConnect) as err:
raise ConfigEntryNotReady from err
@@ -141,15 +143,34 @@ class AugustData(AugustSubscriberMixin):
self._pubnub_unsub = async_create_pubnub(user_data["UserID"], pubnub)
if self._locks_by_id:
tasks = []
for lock_id in self._locks_by_id:
detail = self._device_detail_by_id[lock_id]
tasks.append(
self.async_status_async(
lock_id, bool(detail.bridge and detail.bridge.hyper_bridge)
)
# Do not prevent setup as the sync can timeout
# but it is not a fatal error as the lock
# will recover automatically when it comes back online.
asyncio.create_task(self._async_initial_sync())
async def _async_initial_sync(self):
"""Attempt to request an initial sync."""
# We don't care if this fails because we only want to wake
# locks that are actually online anyways and they will be
# awake when they come back online
for result in await asyncio.gather(
*[
self.async_status_async(
device_id, bool(detail.bridge and detail.bridge.hyper_bridge)
)
for device_id, detail in self._device_detail_by_id.items()
if device_id in self._locks_by_id
],
return_exceptions=True,
):
if isinstance(result, Exception) and not isinstance(
result, (asyncio.TimeoutError, ClientResponseError, CannotConnect)
):
_LOGGER.warning(
"Unexpected exception during initial sync: %s",
result,
exc_info=result,
)
await asyncio.gather(*tasks)
@callback
def async_pubnub_message(self, device_id, date_time, message):
@@ -185,12 +206,28 @@ class AugustData(AugustSubscriberMixin):
await self._async_refresh_device_detail_by_ids(self._subscriptions.keys())
async def _async_refresh_device_detail_by_ids(self, device_ids_list):
await asyncio.gather(
*(
self._async_refresh_device_detail_by_id(device_id)
for device_id in device_ids_list
)
)
"""Refresh each device in sequence.
This used to be a gather but it was less reliable with august's
recent api changes.
The august api has been timing out for some devices so
we want the ones that it isn't timing out for to keep working.
"""
for device_id in device_ids_list:
try:
await self._async_refresh_device_detail_by_id(device_id)
except asyncio.TimeoutError:
_LOGGER.warning(
"Timed out calling august api during refresh of device: %s",
device_id,
)
except (ClientResponseError, CannotConnect) as err:
_LOGGER.warning(
"Error from august api during refresh of device: %s",
device_id,
exc_info=err,
)
async def _async_refresh_device_detail_by_id(self, device_id):
if device_id in self._locks_by_id:

View File

@@ -4,7 +4,7 @@ from datetime import timedelta
from homeassistant.const import Platform
DEFAULT_TIMEOUT = 10
DEFAULT_TIMEOUT = 15
CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file"
CONF_LOGIN_METHOD = "login_method"

View File

@@ -2,7 +2,7 @@
"domain": "august",
"name": "August",
"documentation": "https://www.home-assistant.io/integrations/august",
"requirements": ["yalexs==1.1.19"],
"requirements": ["yalexs==1.1.20"],
"codeowners": ["@bdraco"],
"dhcp": [
{

View File

@@ -8,7 +8,11 @@
{
"hostname": "blink*",
"macaddress": "B85F98*"
}
},
{
"hostname": "blink*",
"macaddress": "00037F*"
}
],
"config_flow": true,
"iot_class": "cloud_polling"

View File

@@ -1,12 +1,21 @@
"""Component to embed Google Cast."""
import logging
from __future__ import annotations
import logging
from typing import Protocol
from pychromecast import Chromecast
import voluptuous as vol
from homeassistant.components.media_player import BrowseMedia
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.integration_platform import (
async_process_integration_platforms,
)
from homeassistant.helpers.typing import ConfigType
from . import home_assistant_cast
@@ -49,9 +58,58 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Cast from a config entry."""
await home_assistant_cast.async_setup_ha_cast(hass, entry)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
hass.data[DOMAIN] = {}
await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform)
return True
class CastProtocol(Protocol):
"""Define the format of cast platforms."""
async def async_get_media_browser_root_object(
self, hass: HomeAssistant, cast_type: str
) -> list[BrowseMedia]:
"""Create a list of root objects for media browsing."""
async def async_browse_media(
self,
hass: HomeAssistant,
media_content_type: str,
media_content_id: str,
cast_type: str,
) -> BrowseMedia | None:
"""Browse media.
Return a BrowseMedia object or None if the media does not belong to this platform.
"""
async def async_play_media(
self,
hass: HomeAssistant,
cast_entity_id: str,
chromecast: Chromecast,
media_type: str,
media_id: str,
) -> bool:
"""Play media.
Return True if the media is played by the platform, False if not.
"""
async def _register_cast_platform(
hass: HomeAssistant, integration_domain: str, platform: CastProtocol
):
"""Register a cast platform."""
if (
not hasattr(platform, "async_get_media_browser_root_object")
or not hasattr(platform, "async_browse_media")
or not hasattr(platform, "async_play_media")
):
raise HomeAssistantError(f"Invalid cast platform {platform}")
hass.data[DOMAIN][integration_domain] = platform
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Remove Home Assistant Cast user."""
await home_assistant_cast.async_remove_user(hass, entry)

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio
from contextlib import suppress
from datetime import datetime, timedelta
import functools as ft
import json
import logging
from urllib.parse import quote
@@ -12,7 +11,6 @@ from urllib.parse import quote
import pychromecast
from pychromecast.controllers.homeassistant import HomeAssistantController
from pychromecast.controllers.multizone import MultizoneManager
from pychromecast.controllers.plex import PlexController
from pychromecast.controllers.receiver import VOLUME_CONTROL_TYPE_FIXED
from pychromecast.quick_play import quick_play
from pychromecast.socket_client import (
@@ -21,7 +19,7 @@ from pychromecast.socket_client import (
)
import voluptuous as vol
from homeassistant.components import media_source, plex, zeroconf
from homeassistant.components import media_source, zeroconf
from homeassistant.components.http.auth import async_sign_path
from homeassistant.components.media_player import (
BrowseError,
@@ -30,7 +28,6 @@ from homeassistant.components.media_player import (
)
from homeassistant.components.media_player.const import (
ATTR_MEDIA_EXTRA,
MEDIA_CLASS_APP,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC,
@@ -48,8 +45,6 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
)
from homeassistant.components.plex.const import PLEX_URI_SCHEME
from homeassistant.components.plex.services import lookup_plex_media
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
@@ -461,55 +456,28 @@ class CastDevice(MediaPlayerEntity):
media_controller = self._media_controller()
media_controller.seek(position)
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
kwargs = {}
async def _async_root_payload(self, content_filter):
"""Generate root node."""
children = []
if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_AUDIO:
kwargs["content_filter"] = lambda item: item.media_content_type.startswith(
"audio/"
)
if media_content_id is not None:
if plex.is_plex_media_id(media_content_id):
return await plex.async_browse_media(
self.hass,
media_content_type,
media_content_id,
platform=CAST_DOMAIN,
)
return await media_source.async_browse_media(
self.hass, media_content_id, **kwargs
)
if media_content_type == "plex":
return await plex.async_browse_media(
self.hass, None, None, platform=CAST_DOMAIN
)
if "plex" in self.hass.config.components:
children.append(
BrowseMedia(
title="Plex",
media_class=MEDIA_CLASS_APP,
media_content_id="",
media_content_type="plex",
thumbnail="https://brands.home-assistant.io/_/plex/logo.png",
can_play=False,
can_expand=True,
# Add media browsers
for platform in self.hass.data[CAST_DOMAIN].values():
children.extend(
await platform.async_get_media_browser_root_object(
self.hass, self._chromecast.cast_type
)
)
# Add media sources
try:
result = await media_source.async_browse_media(
self.hass, media_content_id, **kwargs
self.hass, None, content_filter=content_filter
)
children.append(result)
except BrowseError:
if not children:
raise
# If there's only one media source, resolve it
if len(children) == 1:
return await self.async_browse_media(
children[0].media_content_type,
@@ -526,6 +494,38 @@ class CastDevice(MediaPlayerEntity):
children=children,
)
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
content_filter = None
if self._chromecast.cast_type in (
pychromecast.const.CAST_TYPE_AUDIO,
pychromecast.const.CAST_TYPE_GROUP,
):
def audio_content_filter(item):
"""Filter non audio content."""
return item.media_content_type.startswith("audio/")
content_filter = audio_content_filter
if media_content_id is None:
return await self._async_root_payload(content_filter)
for platform in self.hass.data[CAST_DOMAIN].values():
browse_media = await platform.async_browse_media(
self.hass,
media_content_type,
media_content_id,
self._chromecast.cast_type,
)
if browse_media:
return browse_media
return await media_source.async_browse_media(
self.hass, media_content_id, content_filter=content_filter
)
async def async_play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
# Handle media_source
@@ -547,16 +547,10 @@ class CastDevice(MediaPlayerEntity):
hass_url = get_url(self.hass, prefer_external=True)
media_id = f"{hass_url}{media_id}"
await self.hass.async_add_executor_job(
ft.partial(self.play_media, media_type, media_id, **kwargs)
)
def play_media(self, media_type, media_id, **kwargs):
"""Play media from a URL."""
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
metadata = extra.get("metadata")
# We do not want this to be forwarded to a group
# Handle media supported by a known cast app
if media_type == CAST_DOMAIN:
try:
app_data = json.loads(media_id)
@@ -571,7 +565,9 @@ class CastDevice(MediaPlayerEntity):
if "app_id" in app_data:
app_id = app_data.pop("app_id")
_LOGGER.info("Starting Cast app by ID %s", app_id)
self._chromecast.start_app(app_id)
await self.hass.async_add_executor_job(
self._chromecast.start_app, app_id
)
if app_data:
_LOGGER.warning(
"Extra keys %s were ignored. Please use app_name to cast media",
@@ -581,21 +577,26 @@ class CastDevice(MediaPlayerEntity):
app_name = app_data.pop("app_name")
try:
quick_play(self._chromecast, app_name, app_data)
await self.hass.async_add_executor_job(
quick_play, self._chromecast, app_name, app_data
)
except NotImplementedError:
_LOGGER.error("App %s not supported", app_name)
# Handle plex
elif media_id and media_id.startswith(PLEX_URI_SCHEME):
media_id = media_id[len(PLEX_URI_SCHEME) :]
media = lookup_plex_media(self.hass, media_type, media_id)
if media is None:
return
# Try the cast platforms
for platform in self.hass.data[CAST_DOMAIN].values():
result = await platform.async_play_media(
self.hass, self.entity_id, self._chromecast, media_type, media_id
)
if result:
return
controller = PlexController()
self._chromecast.register_handler(controller)
controller.play_media(media)
else:
app_data = {"media_id": media_id, "media_type": media_type, **extra}
quick_play(self._chromecast, "default_media_receiver", app_data)
# Default to play with the default media receiver
app_data = {"media_id": media_id, "media_type": media_type, **extra}
await self.hass.async_add_executor_job(
quick_play, self._chromecast, "default_media_receiver", app_data
)
def _media_status(self):
"""

View File

@@ -1,5 +1,4 @@
"""Provide configuration end points for Automations."""
from collections import OrderedDict
import uuid
from homeassistant.components.automation.config import (
@@ -52,7 +51,18 @@ class EditAutomationConfigView(EditIdBasedConfigView):
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
index = None
updated_value = {CONF_ID: config_key}
# Iterate through some keys that we want to have ordered in the output
for key in ("alias", "description", "trigger", "condition", "action"):
if key in new_value:
updated_value[key] = new_value[key]
# We cover all current fields above, but just in case we start
# supporting more fields in the future.
updated_value.update(new_value)
updated = False
for index, cur_value in enumerate(data):
# When people copy paste their automations to the config file,
# they sometimes forget to add IDs. Fix it here.
@@ -60,23 +70,8 @@ class EditAutomationConfigView(EditIdBasedConfigView):
cur_value[CONF_ID] = uuid.uuid4().hex
elif cur_value[CONF_ID] == config_key:
break
else:
cur_value = OrderedDict()
cur_value[CONF_ID] = config_key
index = len(data)
data.append(cur_value)
data[index] = updated_value
updated = True
# Iterate through some keys that we want to have ordered in the output
updated_value = OrderedDict()
for key in ("id", "alias", "description", "trigger", "condition", "action"):
if key in cur_value:
updated_value[key] = cur_value[key]
if key in new_value:
updated_value[key] = new_value[key]
# We cover all current fields above, but just in case we start
# supporting more fields in the future.
updated_value.update(cur_value)
updated_value.update(new_value)
data[index] = updated_value
if not updated:
data.append(updated_value)

View File

@@ -47,8 +47,8 @@ class EditSceneConfigView(EditIdBasedConfigView):
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
# Iterate through some keys that we want to have ordered in the output
updated_value = {CONF_ID: config_key}
# Iterate through some keys that we want to have ordered in the output
for key in ("name", "entities"):
if key in new_value:
updated_value[key] = new_value[key]

View File

@@ -46,7 +46,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Enable asyncio debugging and start the debugger."""
get_running_loop().set_debug(True)
debugpy.listen((conf[CONF_HOST], conf[CONF_PORT]))
await hass.async_add_executor_job(
debugpy.listen, (conf[CONF_HOST], conf[CONF_PORT])
)
if conf[CONF_WAIT]:
_LOGGER.warning(

View File

@@ -7,7 +7,7 @@ from typing import Any
from pydeconz.group import DeconzScene as PydeconzScene
from homeassistant.components.scene import Scene
from homeassistant.components.scene import DOMAIN, Scene
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -23,6 +23,7 @@ async def async_setup_entry(
) -> None:
"""Set up scenes for deCONZ component."""
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
@callback
def async_add_scene(
@@ -30,7 +31,11 @@ async def async_setup_entry(
| ValuesView[PydeconzScene] = gateway.api.scenes.values(),
) -> None:
"""Add scene from deCONZ."""
entities = [DeconzScene(scene, gateway) for scene in scenes]
entities = [
DeconzScene(scene, gateway)
for scene in scenes
if scene.deconz_id not in gateway.entities[DOMAIN]
]
if entities:
async_add_entities(entities)
@@ -59,10 +64,12 @@ class DeconzScene(Scene):
async def async_added_to_hass(self) -> None:
"""Subscribe to sensors events."""
self.gateway.deconz_ids[self.entity_id] = self._scene.deconz_id
self.gateway.entities[DOMAIN].add(self._scene.deconz_id)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect scene object when removed."""
del self.gateway.deconz_ids[self.entity_id]
self.gateway.entities[DOMAIN].remove(self._scene.deconz_id)
self._scene = None
async def async_activate(self, **kwargs: Any) -> None:

View File

@@ -170,7 +170,7 @@ async def _async_get_json_file_response(
return web.Response(
body=json_data,
content_type="application/json",
headers={"Content-Disposition": f'attachment; filename="{filename}.json"'},
headers={"Content-Disposition": f'attachment; filename="{filename}.json.txt"'},
)

View File

@@ -2,19 +2,24 @@
from __future__ import annotations
from collections.abc import Iterable, Mapping
from typing import Any
from typing import Any, TypeVar, cast
from homeassistant.core import callback
from .const import REDACTED
T = TypeVar("T")
@callback
def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict[str, Any]:
def async_redact_data(data: T, to_redact: Iterable[Any]) -> T:
"""Redact sensitive data in a dict."""
if not isinstance(data, (Mapping, list)):
return data
if isinstance(data, list):
return cast(T, [async_redact_data(val, to_redact) for val in data])
redacted = {**data}
for key, value in redacted.items():
@@ -25,4 +30,4 @@ def async_redact_data(data: Mapping, to_redact: Iterable[Any]) -> dict[str, Any]
elif isinstance(value, list):
redacted[key] = [async_redact_data(item, to_redact) for item in value]
return redacted
return cast(T, redacted)

View File

@@ -110,11 +110,13 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data={
CONF_HOSTNAME: hostname,
CONF_NAME: name,
CONF_RESOLVER: resolver,
CONF_RESOLVER_IPV6: resolver_ipv6,
CONF_IPV4: validate[CONF_IPV4],
CONF_IPV6: validate[CONF_IPV6],
},
options={
CONF_RESOLVER: resolver,
CONF_RESOLVER_IPV6: resolver_ipv6,
},
)
return self.async_show_form(

View File

@@ -79,10 +79,8 @@ async def async_setup_entry(
hostname = entry.data[CONF_HOSTNAME]
name = entry.data[CONF_NAME]
resolver_ipv4 = entry.options.get(CONF_RESOLVER, entry.data[CONF_RESOLVER])
resolver_ipv6 = entry.options.get(
CONF_RESOLVER_IPV6, entry.data[CONF_RESOLVER_IPV6]
)
resolver_ipv4 = entry.options[CONF_RESOLVER]
resolver_ipv6 = entry.options[CONF_RESOLVER_IPV6]
entities = []
if entry.data[CONF_IPV4]:
entities.append(WanIpSensor(name, hostname, resolver_ipv4, False))

View File

@@ -1,7 +1,9 @@
"""The Flick Electric integration."""
from datetime import datetime as dt
import logging
import jwt
from pyflick import FlickAPI
from pyflick.authentication import AbstractFlickAuth
from pyflick.const import DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET
@@ -18,7 +20,9 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
from .const import CONF_TOKEN_EXPIRES_IN, CONF_TOKEN_EXPIRY, DOMAIN
from .const import CONF_TOKEN_EXPIRY, DOMAIN
_LOGGER = logging.getLogger(__name__)
CONF_ID_TOKEN = "id_token"
@@ -69,6 +73,8 @@ class HassFlickAuth(AbstractFlickAuth):
return self._entry.data[CONF_ACCESS_TOKEN]
async def _update_token(self):
_LOGGER.debug("Fetching new access token")
token = await self.get_new_token(
username=self._entry.data[CONF_USERNAME],
password=self._entry.data[CONF_PASSWORD],
@@ -78,15 +84,19 @@ class HassFlickAuth(AbstractFlickAuth):
),
)
# Reduce expiry by an hour to avoid API being called after expiry
expiry = dt.now().timestamp() + int(token[CONF_TOKEN_EXPIRES_IN] - 3600)
_LOGGER.debug("New token: %s", token)
# Flick will send the same token, but expiry is relative - so grab it from the token
token_decoded = jwt.decode(
token[CONF_ID_TOKEN], options={"verify_signature": False}
)
self._hass.config_entries.async_update_entry(
self._entry,
data={
**self._entry.data,
CONF_ACCESS_TOKEN: token,
CONF_TOKEN_EXPIRY: expiry,
CONF_TOKEN_EXPIRY: token_decoded["exp"],
},
)

View File

@@ -2,7 +2,6 @@
DOMAIN = "flick_electric"
CONF_TOKEN_EXPIRES_IN = "expires_in"
CONF_TOKEN_EXPIRY = "expires"
ATTR_START_AT = "start_at"

View File

@@ -15,8 +15,6 @@ from homeassistant.util.dt import utcnow
from .const import ATTR_COMPONENTS, ATTR_END_AT, ATTR_START_AT, DOMAIN
_LOGGER = logging.getLogger(__name__)
_AUTH_URL = "https://api.flick.energy/identity/oauth/token"
_RESOURCE = "https://api.flick.energy/customer/mobile_provider/price"
SCAN_INTERVAL = timedelta(minutes=5)
@@ -71,6 +69,8 @@ class FlickPricingSensor(SensorEntity):
async with async_timeout.timeout(60):
self._price = await self._api.getPricing()
_LOGGER.debug("Pricing data: %s", self._price)
self._attributes[ATTR_START_AT] = self._price.start_at
self._attributes[ATTR_END_AT] = self._price.end_at
for component in self._price.components:

View File

@@ -14,7 +14,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import (
async_track_time_change,
@@ -88,6 +88,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def _async_migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Migrate entities when the mac address gets discovered."""
unique_id = entry.unique_id
if not unique_id:
return
entry_id = entry.entry_id
@callback
def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None:
# Old format {entry_id}.....
# New format {unique_id}....
entity_unique_id = entity_entry.unique_id
if not entity_unique_id.startswith(entry_id):
return None
new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}"
_LOGGER.info(
"Migrating unique_id from [%s] to [%s]",
entity_unique_id,
new_unique_id,
)
return {"new_unique_id": new_unique_id}
await er.async_migrate_entries(hass, entry.entry_id, _async_migrator)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Flux LED/MagicLight from a config entry."""
host = entry.data[CONF_HOST]
@@ -135,6 +160,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# is either missing or we have verified it matches
async_update_entry_from_discovery(hass, entry, discovery, device.model_num)
await _async_migrate_unique_ids(hass, entry)
coordinator = FluxLedUpdateCoordinator(hass, device, entry)
hass.data[DOMAIN][entry.entry_id] = coordinator
platforms = PLATFORMS_BY_TYPE[device.device_type]

View File

@@ -64,8 +64,8 @@ class FluxButton(FluxBaseEntity, ButtonEntity):
self.entity_description = description
super().__init__(device, entry)
self._attr_name = f"{entry.data[CONF_NAME]} {description.name}"
if entry.unique_id:
self._attr_unique_id = f"{entry.unique_id}_{description.key}"
base_unique_id = entry.unique_id or entry.entry_id
self._attr_unique_id = f"{base_unique_id}_{description.key}"
async def async_press(self) -> None:
"""Send out a command."""

View File

@@ -51,6 +51,7 @@ FLUX_LED_EXCEPTIONS: Final = (
STARTUP_SCAN_TIMEOUT: Final = 5
DISCOVER_SCAN_TIMEOUT: Final = 10
DIRECTED_DISCOVERY_TIMEOUT: Final = 15
CONF_MODEL: Final = "model"
CONF_MODEL_NUM: Final = "model_num"

View File

@@ -38,7 +38,7 @@ from .const import (
CONF_REMOTE_ACCESS_ENABLED,
CONF_REMOTE_ACCESS_HOST,
CONF_REMOTE_ACCESS_PORT,
DISCOVER_SCAN_TIMEOUT,
DIRECTED_DISCOVERY_TIMEOUT,
DOMAIN,
FLUX_LED_DISCOVERY,
)
@@ -194,7 +194,7 @@ async def async_discover_device(
"""Direct discovery at a single ip instead of broadcast."""
# If we are missing the unique_id we should be able to fetch it
# from the device by doing a directed discovery at the host only
for device in await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT, host):
for device in await async_discover_devices(hass, DIRECTED_DISCOVERY_TIMEOUT, host):
if device[ATTR_IPADDR] == host:
return device
return None

View File

@@ -7,19 +7,28 @@ from typing import Any
from flux_led.aiodevice import AIOWifiLedBulb
from homeassistant import config_entries
from homeassistant.const import CONF_NAME
from homeassistant.const import (
ATTR_CONNECTIONS,
ATTR_HW_VERSION,
ATTR_IDENTIFIERS,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_NAME,
ATTR_SW_VERSION,
CONF_NAME,
)
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONF_MINOR_VERSION, CONF_MODEL, SIGNAL_STATE_UPDATED
from .const import CONF_MINOR_VERSION, CONF_MODEL, DOMAIN, SIGNAL_STATE_UPDATED
from .coordinator import FluxLedUpdateCoordinator
def _async_device_info(
unique_id: str, device: AIOWifiLedBulb, entry: config_entries.ConfigEntry
device: AIOWifiLedBulb, entry: config_entries.ConfigEntry
) -> DeviceInfo:
version_num = device.version_num
if minor_version := entry.data.get(CONF_MINOR_VERSION):
@@ -27,14 +36,18 @@ def _async_device_info(
sw_version_str = f"{sw_version:0.2f}"
else:
sw_version_str = str(device.version_num)
return DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, unique_id)},
manufacturer="Zengge",
model=device.model,
name=entry.data[CONF_NAME],
sw_version=sw_version_str,
hw_version=entry.data.get(CONF_MODEL),
)
device_info: DeviceInfo = {
ATTR_IDENTIFIERS: {(DOMAIN, entry.entry_id)},
ATTR_MANUFACTURER: "Zengge",
ATTR_MODEL: device.model,
ATTR_NAME: entry.data[CONF_NAME],
ATTR_SW_VERSION: sw_version_str,
}
if hw_model := entry.data.get(CONF_MODEL):
device_info[ATTR_HW_VERSION] = hw_model
if entry.unique_id:
device_info[ATTR_CONNECTIONS] = {(dr.CONNECTION_NETWORK_MAC, entry.unique_id)}
return device_info
class FluxBaseEntity(Entity):
@@ -50,10 +63,7 @@ class FluxBaseEntity(Entity):
"""Initialize the light."""
self._device: AIOWifiLedBulb = device
self.entry = entry
if entry.unique_id:
self._attr_device_info = _async_device_info(
entry.unique_id, self._device, entry
)
self._attr_device_info = _async_device_info(self._device, entry)
class FluxEntity(CoordinatorEntity):
@@ -64,7 +74,7 @@ class FluxEntity(CoordinatorEntity):
def __init__(
self,
coordinator: FluxLedUpdateCoordinator,
unique_id: str | None,
base_unique_id: str,
name: str,
key: str | None,
) -> None:
@@ -74,13 +84,10 @@ class FluxEntity(CoordinatorEntity):
self._responding = True
self._attr_name = name
if key:
self._attr_unique_id = f"{unique_id}_{key}"
self._attr_unique_id = f"{base_unique_id}_{key}"
else:
self._attr_unique_id = unique_id
if unique_id:
self._attr_device_info = _async_device_info(
unique_id, self._device, coordinator.entry
)
self._attr_unique_id = base_unique_id
self._attr_device_info = _async_device_info(self._device, coordinator.entry)
async def _async_ensure_device_on(self) -> None:
"""Turn the device on if it needs to be turned on before a command."""

View File

@@ -177,7 +177,7 @@ async def async_setup_entry(
[
FluxLight(
coordinator,
entry.unique_id,
entry.unique_id or entry.entry_id,
entry.data[CONF_NAME],
list(custom_effect_colors),
options.get(CONF_CUSTOM_EFFECT_SPEED_PCT, DEFAULT_EFFECT_SPEED),
@@ -195,14 +195,14 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
def __init__(
self,
coordinator: FluxLedUpdateCoordinator,
unique_id: str | None,
base_unique_id: str,
name: str,
custom_effect_colors: list[tuple[int, int, int]],
custom_effect_speed_pct: int,
custom_effect_transition: str,
) -> None:
"""Initialize the light."""
super().__init__(coordinator, unique_id, name, None)
super().__init__(coordinator, base_unique_id, name, None)
self._attr_min_mireds = color_temperature_kelvin_to_mired(self._device.max_temp)
self._attr_max_mireds = color_temperature_kelvin_to_mired(self._device.min_temp)
self._attr_supported_color_modes = _hass_color_modes(self._device)

View File

@@ -51,26 +51,28 @@ async def async_setup_entry(
| FluxMusicSegmentsNumber
] = []
name = entry.data[CONF_NAME]
unique_id = entry.unique_id
base_unique_id = entry.unique_id or entry.entry_id
if device.pixels_per_segment is not None:
entities.append(
FluxPixelsPerSegmentNumber(
coordinator,
unique_id,
base_unique_id,
f"{name} Pixels Per Segment",
"pixels_per_segment",
)
)
if device.segments is not None:
entities.append(
FluxSegmentsNumber(coordinator, unique_id, f"{name} Segments", "segments")
FluxSegmentsNumber(
coordinator, base_unique_id, f"{name} Segments", "segments"
)
)
if device.music_pixels_per_segment is not None:
entities.append(
FluxMusicPixelsPerSegmentNumber(
coordinator,
unique_id,
base_unique_id,
f"{name} Music Pixels Per Segment",
"music_pixels_per_segment",
)
@@ -78,12 +80,12 @@ async def async_setup_entry(
if device.music_segments is not None:
entities.append(
FluxMusicSegmentsNumber(
coordinator, unique_id, f"{name} Music Segments", "music_segments"
coordinator, base_unique_id, f"{name} Music Segments", "music_segments"
)
)
if device.effect_list and device.effect_list != [EFFECT_RANDOM]:
entities.append(
FluxSpeedNumber(coordinator, unique_id, f"{name} Effect Speed", None)
FluxSpeedNumber(coordinator, base_unique_id, f"{name} Effect Speed", None)
)
if entities:
@@ -131,12 +133,12 @@ class FluxConfigNumber(FluxEntity, CoordinatorEntity, NumberEntity):
def __init__(
self,
coordinator: FluxLedUpdateCoordinator,
unique_id: str | None,
base_unique_id: str,
name: str,
key: str | None,
) -> None:
"""Initialize the flux number."""
super().__init__(coordinator, unique_id, name, key)
super().__init__(coordinator, base_unique_id, name, key)
self._debouncer: Debouncer | None = None
self._pending_value: int | None = None

View File

@@ -54,28 +54,28 @@ async def async_setup_entry(
| FluxWhiteChannelSelect
] = []
name = entry.data[CONF_NAME]
unique_id = entry.unique_id
base_unique_id = entry.unique_id or entry.entry_id
if device.device_type == DeviceType.Switch:
entities.append(FluxPowerStateSelect(coordinator.device, entry))
if device.operating_modes:
entities.append(
FluxOperatingModesSelect(
coordinator, unique_id, f"{name} Operating Mode", "operating_mode"
coordinator, base_unique_id, f"{name} Operating Mode", "operating_mode"
)
)
if device.wirings:
entities.append(
FluxWiringsSelect(coordinator, unique_id, f"{name} Wiring", "wiring")
FluxWiringsSelect(coordinator, base_unique_id, f"{name} Wiring", "wiring")
)
if device.ic_types:
entities.append(
FluxICTypeSelect(coordinator, unique_id, f"{name} IC Type", "ic_type")
FluxICTypeSelect(coordinator, base_unique_id, f"{name} IC Type", "ic_type")
)
if device.remote_config:
entities.append(
FluxRemoteConfigSelect(
coordinator, unique_id, f"{name} Remote Config", "remote_config"
coordinator, base_unique_id, f"{name} Remote Config", "remote_config"
)
)
if FLUX_COLOR_MODE_RGBW in device.color_modes:
@@ -111,8 +111,8 @@ class FluxPowerStateSelect(FluxConfigAtStartSelect, SelectEntity):
"""Initialize the power state select."""
super().__init__(device, entry)
self._attr_name = f"{entry.data[CONF_NAME]} Power Restored"
if entry.unique_id:
self._attr_unique_id = f"{entry.unique_id}_power_restored"
base_unique_id = entry.unique_id or entry.entry_id
self._attr_unique_id = f"{base_unique_id}_power_restored"
self._async_set_current_option_from_device()
@callback
@@ -201,12 +201,12 @@ class FluxRemoteConfigSelect(FluxConfigSelect):
def __init__(
self,
coordinator: FluxLedUpdateCoordinator,
unique_id: str | None,
base_unique_id: str,
name: str,
key: str,
) -> None:
"""Initialize the remote config type select."""
super().__init__(coordinator, unique_id, name, key)
super().__init__(coordinator, base_unique_id, name, key)
assert self._device.remote_config is not None
self._name_to_state = {
_human_readable_option(option.name): option for option in RemoteConfig
@@ -238,8 +238,8 @@ class FluxWhiteChannelSelect(FluxConfigAtStartSelect):
"""Initialize the white channel select."""
super().__init__(device, entry)
self._attr_name = f"{entry.data[CONF_NAME]} White Channel"
if entry.unique_id:
self._attr_unique_id = f"{entry.unique_id}_white_channel"
base_unique_id = entry.unique_id or entry.entry_id
self._attr_unique_id = f"{base_unique_id}_white_channel"
@property
def current_option(self) -> str | None:

View File

@@ -25,7 +25,7 @@ async def async_setup_entry(
[
FluxPairedRemotes(
coordinator,
entry.unique_id,
entry.unique_id or entry.entry_id,
f"{entry.data[CONF_NAME]} Paired Remotes",
"paired_remotes",
)

View File

@@ -34,18 +34,18 @@ async def async_setup_entry(
"""Set up the Flux lights."""
coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
entities: list[FluxSwitch | FluxRemoteAccessSwitch | FluxMusicSwitch] = []
unique_id = entry.unique_id
base_unique_id = entry.unique_id or entry.entry_id
name = entry.data[CONF_NAME]
if coordinator.device.device_type == DeviceType.Switch:
entities.append(FluxSwitch(coordinator, unique_id, name, None))
entities.append(FluxSwitch(coordinator, base_unique_id, name, None))
if entry.data.get(CONF_REMOTE_ACCESS_HOST):
entities.append(FluxRemoteAccessSwitch(coordinator.device, entry))
if coordinator.device.microphone:
entities.append(
FluxMusicSwitch(coordinator, unique_id, f"{name} Music", "music")
FluxMusicSwitch(coordinator, base_unique_id, f"{name} Music", "music")
)
if entities:
@@ -74,8 +74,8 @@ class FluxRemoteAccessSwitch(FluxBaseEntity, SwitchEntity):
"""Initialize the light."""
super().__init__(device, entry)
self._attr_name = f"{entry.data[CONF_NAME]} Remote Access"
if entry.unique_id:
self._attr_unique_id = f"{entry.unique_id}_remote_access"
base_unique_id = entry.unique_id or entry.entry_id
self._attr_unique_id = f"{base_unique_id}_remote_access"
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the remote access on."""

View File

@@ -1,11 +1,7 @@
"""Support for AVM Fritz!Box functions."""
import logging
from fritzconnection.core.exceptions import (
FritzConnectionException,
FritzResourceError,
FritzSecurityError,
)
from fritzconnection.core.exceptions import FritzSecurityError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
@@ -13,7 +9,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .common import AvmWrapper, FritzData
from .const import DATA_FRITZ, DOMAIN, PLATFORMS
from .const import DATA_FRITZ, DOMAIN, FRITZ_EXCEPTIONS, PLATFORMS
from .services import async_setup_services, async_unload_services
_LOGGER = logging.getLogger(__name__)
@@ -34,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await avm_wrapper.async_setup(entry.options)
except FritzSecurityError as ex:
raise ConfigEntryAuthFailed from ex
except (FritzConnectionException, FritzResourceError) as ex:
except FRITZ_EXCEPTIONS as ex:
raise ConfigEntryNotReady from ex
hass.data.setdefault(DOMAIN, {})

View File

@@ -12,10 +12,7 @@ from typing import Any, TypedDict, cast
from fritzconnection import FritzConnection
from fritzconnection.core.exceptions import (
FritzActionError,
FritzActionFailedError,
FritzConnectionException,
FritzInternalError,
FritzLookUpError,
FritzSecurityError,
FritzServiceError,
)
@@ -46,6 +43,7 @@ from .const import (
DEFAULT_PORT,
DEFAULT_USERNAME,
DOMAIN,
FRITZ_EXCEPTIONS,
SERVICE_CLEANUP,
SERVICE_REBOOT,
SERVICE_RECONNECT,
@@ -107,7 +105,7 @@ class Device:
ip_address: str
name: str
ssid: str | None
wan_access: bool = True
wan_access: bool | None = None
class Interface(TypedDict):
@@ -188,9 +186,26 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
_LOGGER.error("Unable to establish a connection with %s", self.host)
return
_LOGGER.debug(
"detected services on %s %s",
self.host,
list(self.connection.services.keys()),
)
self.fritz_hosts = FritzHosts(fc=self.connection)
self.fritz_status = FritzStatus(fc=self.connection)
info = self.connection.call_action("DeviceInfo:1", "GetInfo")
_LOGGER.debug(
"gathered device info of %s %s",
self.host,
{
**info,
"NewDeviceLog": "***omitted***",
"NewSerialNumber": "***omitted***",
},
)
if not self._unique_id:
self._unique_id = info["NewSerialNumber"]
@@ -277,6 +292,14 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
)
return bool(version), version
def _get_wan_access(self, ip_address: str) -> bool | None:
"""Get WAN access rule for given IP address."""
return not self.connection.call_action(
"X_AVM-DE_HostFilter:1",
"GetWANAccessByIP",
NewIPv4Address=ip_address,
).get("NewDisallow")
async def async_scan_devices(self, now: datetime | None = None) -> None:
"""Wrap up FritzboxTools class scan."""
await self.hass.async_add_executor_job(self.scan_devices, now)
@@ -315,7 +338,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
connection_type="",
ip_address=host["ip"],
ssid=None,
wan_access=False,
wan_access=None,
)
mesh_intf = {}
@@ -343,32 +366,33 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
for interf in node["node_interfaces"]:
dev_mac = interf["mac_address"]
if dev_mac not in hosts:
continue
dev_info: Device = hosts[dev_mac]
if dev_info.ip_address:
dev_info.wan_access = self._get_wan_access(dev_info.ip_address)
for link in interf["node_links"]:
intf = mesh_intf.get(link["node_interface_1_uid"])
if (
intf is not None
and link["state"] == "CONNECTED"
and dev_mac in hosts
):
dev_info: Device = hosts[dev_mac]
if intf["op_mode"] != "AP_GUEST":
dev_info.wan_access = not self.connection.call_action(
"X_AVM-DE_HostFilter:1",
"GetWANAccessByIP",
NewIPv4Address=dev_info.ip_address,
).get("NewDisallow")
if intf is not None:
if intf["op_mode"] == "AP_GUEST":
dev_info.wan_access = None
dev_info.connected_to = intf["device"]
dev_info.connection_type = intf["type"]
dev_info.ssid = intf.get("ssid")
_LOGGER.debug("Client dev_info: %s", dev_info)
if dev_mac in self._devices:
self._devices[dev_mac].update(dev_info, consider_home)
else:
device = FritzDevice(dev_mac, dev_info.name)
device.update(dev_info, consider_home)
self._devices[dev_mac] = device
new_device = True
if dev_mac in self._devices:
self._devices[dev_mac].update(dev_info, consider_home)
else:
device = FritzDevice(dev_mac, dev_info.name)
device.update(dev_info, consider_home)
self._devices[dev_mac] = device
new_device = True
dispatcher_send(self.hass, self.signal_device_update)
if new_device:
@@ -521,13 +545,7 @@ class AvmWrapper(FritzBoxTools):
"Authorization Error: Please check the provided credentials and verify that you can log into the web interface",
exc_info=True,
)
except (
FritzActionError,
FritzActionFailedError,
FritzInternalError,
FritzServiceError,
FritzLookUpError,
):
except FRITZ_EXCEPTIONS:
_LOGGER.error(
"Service/Action Error: cannot execute service %s with action %s",
service_name,
@@ -760,7 +778,7 @@ class FritzDevice:
self._mac = mac
self._name = name
self._ssid: str | None = None
self._wan_access = False
self._wan_access: bool | None = False
def update(self, dev_info: Device, consider_home: float) -> None:
"""Update device info."""
@@ -828,7 +846,7 @@ class FritzDevice:
return self._ssid
@property
def wan_access(self) -> bool:
def wan_access(self) -> bool | None:
"""Return device wan access."""
return self._wan_access

View File

@@ -2,6 +2,14 @@
from typing import Literal
from fritzconnection.core.exceptions import (
FritzActionError,
FritzActionFailedError,
FritzInternalError,
FritzLookUpError,
FritzServiceError,
)
from homeassistant.backports.enum import StrEnum
from homeassistant.const import Platform
@@ -47,3 +55,11 @@ SWITCH_TYPE_PORTFORWARD = "PortForward"
SWITCH_TYPE_WIFINETWORK = "WiFiNetwork"
UPTIME_DEVIATION = 5
FRITZ_EXCEPTIONS = (
FritzActionError,
FritzActionFailedError,
FritzInternalError,
FritzServiceError,
FritzLookUpError,
)

View File

@@ -0,0 +1,35 @@
"""Diagnostics support for AVM FRITZ!Box."""
from __future__ import annotations
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .common import AvmWrapper
from .const import DOMAIN
TO_REDACT = {CONF_USERNAME, CONF_PASSWORD}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict:
"""Return diagnostics for a config entry."""
avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id]
diag_data = {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"device_info": {
"model": avm_wrapper.model,
"current_firmware": avm_wrapper.current_firmware,
"latest_firmware": avm_wrapper.latest_firmware,
"update_available": avm_wrapper.update_available,
"is_router": avm_wrapper.device_is_router,
"mesh_role": avm_wrapper.mesh_role,
"last_update success": avm_wrapper.last_update_success,
"last_exception": avm_wrapper.last_exception,
},
}
return diag_data

View File

@@ -477,10 +477,17 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
self._attr_entity_category = EntityCategory.CONFIG
@property
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Switch status."""
return self._avm_wrapper.devices[self._mac].wan_access
@property
def available(self) -> bool:
"""Return availability of the switch."""
if self._avm_wrapper.devices[self._mac].wan_access is None:
return False
return super().available
@property
def device_info(self) -> DeviceInfo:
"""Return the device information."""

View File

@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20220127.0"
"home-assistant-frontend==20220201.0"
],
"dependencies": [
"api",

View File

@@ -68,6 +68,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured(
updates={CONF_IP_ADDRESS: self.discovery_info[CONF_IP_ADDRESS]}
)
self._async_abort_entries_match(
{CONF_IP_ADDRESS: self.discovery_info[CONF_IP_ADDRESS]}
)
else:
self._abort_if_unique_id_configured()
@@ -103,6 +106,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_IP_ADDRESS: discovery_info.ip,
CONF_PORT: DEFAULT_PORT,
}
await self._async_set_unique_id(
async_get_pin_from_uid(discovery_info.macaddress.replace(":", "").upper())
)
return await self._async_handle_discovery()
async def async_step_zeroconf(

View File

@@ -8,6 +8,7 @@ import aiohwenergy
import async_timeout
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, UPDATE_INTERVAL, DeviceResponseEntry
@@ -28,7 +29,9 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry]
"""Initialize Update Coordinator."""
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL)
self.api = aiohwenergy.HomeWizardEnergy(host)
session = async_get_clientsession(hass)
self.api = aiohwenergy.HomeWizardEnergy(host, clientsession=session)
async def _async_update_data(self) -> DeviceResponseEntry:
"""Fetch all device and sensor data from api."""

View File

@@ -0,0 +1,34 @@
"""Diagnostics support for P1 Monitor."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import HWEnergyDeviceUpdateCoordinator
TO_REDACT = {CONF_IP_ADDRESS, "serial", "wifi_ssid"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
meter_data = {
"device": coordinator.api.device.todict(),
"data": coordinator.api.data.todict(),
"state": coordinator.api.state.todict()
if coordinator.api.state is not None
else None,
}
return {
"entry": async_redact_data(entry.data, TO_REDACT),
"data": async_redact_data(meter_data, TO_REDACT),
}

View File

@@ -5,7 +5,7 @@
"codeowners": ["@DCSBL"],
"dependencies": [],
"requirements": [
"aiohwenergy==0.7.0"
"aiohwenergy==0.8.0"
],
"zeroconf": ["_hwenergy._tcp.local."],
"config_flow": true,

View File

@@ -242,7 +242,7 @@ class HoneywellUSThermostat(ClimateEntity):
# Get current mode
mode = self._device.system_mode
# Set hold if this is not the case
if getattr(self._device, f"hold_{mode}") is False:
if getattr(self._device, f"hold_{mode}", None) is False:
# Get next period key
next_period_key = f"{mode.capitalize()}NextPeriod"
# Get next period raw value

View File

@@ -49,11 +49,12 @@ class HueBridge:
self.logger = logging.getLogger(__name__)
# store actual api connection to bridge as api
app_key: str = self.config_entry.data[CONF_API_KEY]
websession = aiohttp_client.async_get_clientsession(hass)
if self.api_version == 1:
self.api = HueBridgeV1(self.host, app_key, websession)
self.api = HueBridgeV1(
self.host, app_key, aiohttp_client.async_get_clientsession(hass)
)
else:
self.api = HueBridgeV2(self.host, app_key, websession)
self.api = HueBridgeV2(self.host, app_key)
# store (this) bridge object in hass data
hass.data.setdefault(DOMAIN, {})[self.config_entry.entry_id] = self

View File

@@ -3,7 +3,7 @@
"name": "Philips Hue",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hue",
"requirements": ["aiohue==3.0.11"],
"requirements": ["aiohue==4.0.1"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@@ -76,7 +76,6 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N
"""Perform migration of devices and entities to V2 Id's."""
host = entry.data[CONF_HOST]
api_key = entry.data[CONF_API_KEY]
websession = aiohttp_client.async_get_clientsession(hass)
dev_reg = async_get_device_registry(hass)
ent_reg = async_get_entity_registry(hass)
LOGGER.info("Start of migration of devices and entities to support API schema 2")
@@ -93,7 +92,7 @@ async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> N
dev_ids[normalized_mac] = hass_dev.id
# initialize bridge connection just for the migration
async with HueBridgeV2(host, api_key, websession) as api:
async with HueBridgeV2(host, api_key) as api:
sensor_class_mapping = {
SensorDeviceClass.BATTERY.value: ResourceTypes.DEVICE_POWER,

View File

@@ -158,6 +158,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
existing_entry = await self.async_set_unique_id(isy_mac)
if not existing_entry:
return
if existing_entry.source == config_entries.SOURCE_IGNORE:
raise data_entry_flow.AbortFlow("already_configured")
parsed_url = urlparse(existing_entry.data[CONF_HOST])
if parsed_url.hostname != ip_address:
new_netloc = ip_address

View File

@@ -2,10 +2,12 @@
from __future__ import annotations
from collections.abc import Callable
import logging
from xknx import XKNX
from xknx.devices import DateTime, ExposeSensor
from xknx.dpt import DPTNumeric
from xknx.dpt import DPTNumeric, DPTString
from xknx.exceptions import ConversionError
from xknx.remote_value import RemoteValueSensor
from homeassistant.const import (
@@ -22,6 +24,8 @@ from homeassistant.helpers.typing import ConfigType, StateType
from .const import KNX_ADDRESS
from .schema import ExposeSchema
_LOGGER = logging.getLogger(__name__)
@callback
def create_knx_exposure(
@@ -101,7 +105,10 @@ class KNXExposeSensor:
"""Initialize state of the exposure."""
init_state = self.hass.states.get(self.entity_id)
state_value = self._get_expose_value(init_state)
self.device.sensor_value.value = state_value
try:
self.device.sensor_value.value = state_value
except ConversionError:
_LOGGER.exception("Error during sending of expose sensor value")
@callback
def shutdown(self) -> None:
@@ -132,6 +139,13 @@ class KNXExposeSensor:
and issubclass(self.device.sensor_value.dpt_class, DPTNumeric)
):
return float(value)
if (
value is not None
and isinstance(self.device.sensor_value, RemoteValueSensor)
and issubclass(self.device.sensor_value.dpt_class, DPTString)
):
# DPT 16.000 only allows up to 14 Bytes
return str(value)[:14]
return value
async def _async_entity_changed(self, event: Event) -> None:
@@ -148,9 +162,10 @@ class KNXExposeSensor:
async def _async_set_knx_value(self, value: StateType) -> None:
"""Set new value on xknx ExposeSensor."""
if value is None:
return
await self.device.set(value)
try:
await self.device.set(value)
except ConversionError:
_LOGGER.exception("Error during sending of expose sensor value")
class KNXExposeTime:

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/knx",
"requirements": [
"xknx==0.19.0"
"xknx==0.19.1"
],
"codeowners": [
"@Julius2342",

View File

@@ -222,17 +222,15 @@ class MotionEyeOptionsFlow(OptionsFlow):
if self.show_advanced_options:
# The input URL is not validated as being a URL, to allow for the possibility
# the template input won't be a valid URL until after it's rendered.
schema.update(
{
vol.Required(
CONF_STREAM_URL_TEMPLATE,
default=self._config_entry.options.get(
CONF_STREAM_URL_TEMPLATE,
"",
),
): str
# the template input won't be a valid URL until after it's rendered
stream_kwargs = {}
if CONF_STREAM_URL_TEMPLATE in self._config_entry.options:
stream_kwargs["description"] = {
"suggested_value": self._config_entry.options[
CONF_STREAM_URL_TEMPLATE
]
}
)
schema[vol.Optional(CONF_STREAM_URL_TEMPLATE, **stream_kwargs)] = str
return self.async_show_form(step_id="init", data_schema=vol.Schema(schema))

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Any
from google_nest_sdm import diagnostics
from google_nest_sdm.device import Device
from google_nest_sdm.device_traits import InfoTrait
from google_nest_sdm.exceptions import ApiException
@@ -30,22 +31,14 @@ async def async_get_config_entry_diagnostics(
return {"error": str(err)}
return {
**diagnostics.get_diagnostics(),
"devices": [
get_device_data(device) for device in device_manager.devices.values()
]
],
}
def get_device_data(device: Device) -> dict[str, Any]:
"""Return diagnostic information about a device."""
# Return a simplified view of the API object, but skipping any id fields or
# traits that include unique identifiers or personally identifiable information.
# See https://developers.google.com/nest/device-access/traits for API details
return {
"type": device.type,
"traits": {
trait: data
for trait, data in device.raw_data.get("traits", {}).items()
if trait not in REDACT_DEVICE_TRAITS
},
}
# Library performs its own redaction for device data
return device.get_diagnostics()

View File

@@ -3,7 +3,7 @@
"name": "Netatmo",
"documentation": "https://www.home-assistant.io/integrations/netatmo",
"requirements": [
"pyatmo==6.2.2"
"pyatmo==6.2.4"
],
"after_dependencies": [
"cloud",

View File

@@ -2,7 +2,7 @@
"domain": "netgear",
"name": "NETGEAR",
"documentation": "https://www.home-assistant.io/integrations/netgear",
"requirements": ["pynetgear==0.9.0"],
"requirements": ["pynetgear==0.9.1"],
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
"iot_class": "local_polling",
"config_flow": true,

View File

@@ -2,6 +2,10 @@
"domain": "oncue",
"name": "Oncue by Kohler",
"config_flow": true,
"dhcp": [{
"hostname": "kohlergen*",
"macaddress": "00146F*"
}],
"documentation": "https://www.home-assistant.io/integrations/oncue",
"requirements": ["aiooncue==0.3.2"],
"codeowners": ["@bdraco"],

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/overkiz",
"requirements": [
"pyoverkiz==1.3.1"
"pyoverkiz==1.3.2"
],
"zeroconf": [
{

View File

@@ -0,0 +1,77 @@
"""Google Cast support for the Plex component."""
from __future__ import annotations
from pychromecast import Chromecast
from pychromecast.controllers.plex import PlexController
from homeassistant.components.cast.const import DOMAIN as CAST_DOMAIN
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import MEDIA_CLASS_APP
from homeassistant.core import HomeAssistant
from . import async_browse_media as async_browse_plex_media, is_plex_media_id
from .const import PLEX_URI_SCHEME
from .services import lookup_plex_media
async def async_get_media_browser_root_object(
hass: HomeAssistant, cast_type: str
) -> list[BrowseMedia]:
"""Create a root object for media browsing."""
return [
BrowseMedia(
title="Plex",
media_class=MEDIA_CLASS_APP,
media_content_id="",
media_content_type="plex",
thumbnail="https://brands.home-assistant.io/_/plex/logo.png",
can_play=False,
can_expand=True,
)
]
async def async_browse_media(
hass: HomeAssistant,
media_content_type: str,
media_content_id: str,
cast_type: str,
) -> BrowseMedia | None:
"""Browse media."""
if is_plex_media_id(media_content_id):
return await async_browse_plex_media(
hass, media_content_type, media_content_id, platform=CAST_DOMAIN
)
if media_content_type == "plex":
return await async_browse_plex_media(hass, None, None, platform=CAST_DOMAIN)
return None
def _play_media(
hass: HomeAssistant, chromecast: Chromecast, media_type: str, media_id: str
) -> None:
"""Play media."""
media_id = media_id[len(PLEX_URI_SCHEME) :]
media = lookup_plex_media(hass, media_type, media_id)
if media is None:
return
controller = PlexController()
chromecast.register_handler(controller)
controller.play_media(media)
async def async_play_media(
hass: HomeAssistant,
cast_entity_id: str,
chromecast: Chromecast,
media_type: str,
media_id: str,
) -> bool:
"""Play media."""
if media_id and media_id.startswith(PLEX_URI_SCHEME):
await hass.async_add_executor_job(
_play_media, hass, chromecast, media_type, media_id
)
return True
return False

View File

@@ -5,6 +5,7 @@ import logging
import requests
from tesla_powerwall import (
AccessDeniedError,
APIError,
MissingAttributeError,
Powerwall,
PowerwallUnreachableError,
@@ -131,7 +132,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
power_wall = Powerwall(ip_address, http_session=http_session)
runtime_data[POWERWALL_OBJECT] = power_wall
runtime_data[POWERWALL_HTTP_SESSION] = http_session
power_wall.login("", password)
power_wall.login(password)
async def _async_login_and_retry_update_data():
"""Retry the update after a failed login."""
nonlocal login_failed_count
# If the session expired, recreate, relogin, and try again
_LOGGER.debug("Retrying login and updating data")
try:
await hass.async_add_executor_job(_recreate_powerwall_login)
data = await _async_update_powerwall_data(hass, entry, power_wall)
except AccessDeniedError as err:
login_failed_count += 1
if login_failed_count == MAX_LOGIN_FAILURES:
raise ConfigEntryAuthFailed from err
raise UpdateFailed(
f"Login attempt {login_failed_count}/{MAX_LOGIN_FAILURES} failed, will retry: {err}"
) from err
except APIError as err:
raise UpdateFailed(f"Updated failed due to {err}, will retry") from err
else:
login_failed_count = 0
return data
async def async_update_data():
"""Fetch data from API endpoint."""
@@ -147,18 +169,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except AccessDeniedError as err:
if password is None:
raise ConfigEntryAuthFailed from err
# If the session expired, recreate, relogin, and try again
try:
await hass.async_add_executor_job(_recreate_powerwall_login)
return await _async_update_powerwall_data(hass, entry, power_wall)
except AccessDeniedError as ex:
login_failed_count += 1
if login_failed_count == MAX_LOGIN_FAILURES:
raise ConfigEntryAuthFailed from ex
raise UpdateFailed(
f"Login attempt {login_failed_count}/{MAX_LOGIN_FAILURES} failed, will retry"
) from ex
return await _async_login_and_retry_update_data()
except APIError as err:
raise UpdateFailed(f"Updated failed due to {err}, will retry") from err
else:
login_failed_count = 0
return data

View File

@@ -3,7 +3,7 @@
"name": "Sony PlayStation 4",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ps4",
"requirements": ["pyps4-2ndscreen==1.2.0"],
"requirements": ["pyps4-2ndscreen==1.3.1"],
"codeowners": ["@ktnrg45"],
"iot_class": "local_polling"
}

View File

@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_SYSTEM_ID, DOMAIN
from .const import CONF_SYSTEM_ID, DOMAIN, LOGGER
async def validate_input(hass: HomeAssistant, *, api_key: str, system_id: int) -> None:
@@ -50,6 +50,7 @@ class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN):
except PVOutputAuthenticationError:
errors["base"] = "invalid_auth"
except PVOutputError:
LOGGER.exception("Cannot connect to PVOutput")
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(str(user_input[CONF_SYSTEM_ID]))

View File

@@ -13,7 +13,11 @@
{
"hostname": "roomba-*",
"macaddress": "80A589*"
}
},
{
"hostname": "roomba-*",
"macaddress": "DCF505*"
}
],
"iot_class": "local_push"
}

View File

@@ -5,6 +5,7 @@ from abc import ABC, abstractmethod
import contextlib
from typing import Any
from requests.exceptions import Timeout as RequestsTimeout
from samsungctl import Remote
from samsungctl.exceptions import AccessDenied, ConnectionClosed, UnhandledResponse
from samsungtvws import SamsungTVWS
@@ -321,7 +322,7 @@ class SamsungTVWSBridge(SamsungTVBridge):
def device_info(self) -> dict[str, Any] | None:
"""Try to gather infos of this TV."""
if remote := self._get_remote(avoid_open=True):
with contextlib.suppress(HttpApiError):
with contextlib.suppress(HttpApiError, RequestsTimeout):
device_info: dict[str, Any] = remote.rest_device_info()
return device_info

View File

@@ -0,0 +1,18 @@
"""Diagnostics support for SamsungTV."""
from __future__ import annotations
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
TO_REDACT = {CONF_TOKEN}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict:
"""Return diagnostics for a config entry."""
diag_data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)}
return diag_data

View File

@@ -31,50 +31,30 @@ async def async_setup_entry(
) -> None:
"""Set up SenseME lights."""
device = hass.data[DOMAIN][entry.entry_id]
if device.has_light:
async_add_entities([HASensemeLight(device)])
if not device.has_light:
return
if device.is_light:
async_add_entities([HASensemeStandaloneLight(device)])
else:
async_add_entities([HASensemeFanLight(device)])
class HASensemeLight(SensemeEntity, LightEntity):
"""Representation of a Big Ass Fans SenseME light."""
def __init__(self, device: SensemeDevice) -> None:
def __init__(self, device: SensemeDevice, name: str) -> None:
"""Initialize the entity."""
self._device = device
if device.is_light:
name = device.name # The device itself is a light
else:
name = f"{device.name} Light" # A fan light
super().__init__(device, name)
if device.is_light:
self._attr_supported_color_modes = {COLOR_MODE_COLOR_TEMP}
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
else:
self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS}
self._attr_color_mode = COLOR_MODE_BRIGHTNESS
self._attr_unique_id = f"{self._device.uuid}-LIGHT" # for legacy compat
self._attr_min_mireds = color_temperature_kelvin_to_mired(
self._device.light_color_temp_max
)
self._attr_max_mireds = color_temperature_kelvin_to_mired(
self._device.light_color_temp_min
)
self._attr_unique_id = f"{device.uuid}-LIGHT" # for legacy compat
@callback
def _async_update_attrs(self) -> None:
"""Update attrs from device."""
self._attr_is_on = self._device.light_on
self._attr_brightness = int(min(255, self._device.light_brightness * 16))
self._attr_color_temp = color_temperature_kelvin_to_mired(
self._device.light_color_temp
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None:
self._device.light_color_temp = color_temperature_mired_to_kelvin(
color_temp
)
if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None:
# set the brightness, which will also turn on/off light
if brightness == 255:
@@ -86,3 +66,45 @@ class HASensemeLight(SensemeEntity, LightEntity):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
self._device.light_on = False
class HASensemeFanLight(HASensemeLight):
"""Representation of a Big Ass Fans SenseME light on a fan."""
def __init__(self, device: SensemeDevice) -> None:
"""Init a fan light."""
super().__init__(device, device.name)
self._attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS}
self._attr_color_mode = COLOR_MODE_BRIGHTNESS
class HASensemeStandaloneLight(HASensemeLight):
"""Representation of a Big Ass Fans SenseME light."""
def __init__(self, device: SensemeDevice) -> None:
"""Init a standalone light."""
super().__init__(device, f"{device.name} Light")
self._attr_supported_color_modes = {COLOR_MODE_COLOR_TEMP}
self._attr_color_mode = COLOR_MODE_COLOR_TEMP
self._attr_min_mireds = color_temperature_kelvin_to_mired(
device.light_color_temp_max
)
self._attr_max_mireds = color_temperature_kelvin_to_mired(
device.light_color_temp_min
)
@callback
def _async_update_attrs(self) -> None:
"""Update attrs from device."""
super()._async_update_attrs()
self._attr_color_temp = color_temperature_kelvin_to_mired(
self._device.light_color_temp
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None:
self._device.light_color_temp = color_temperature_mired_to_kelvin(
color_temp
)
await super().async_turn_on(**kwargs)

View File

@@ -0,0 +1,40 @@
"""Diagnostics support for SimpliSafe."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS
from homeassistant.core import HomeAssistant
from . import SimpliSafe
from .const import DOMAIN
CONF_SERIAL = "serial"
CONF_SYSTEM_ID = "system_id"
CONF_WIFI_SSID = "wifi_ssid"
TO_REDACT = {
CONF_ADDRESS,
CONF_SERIAL,
CONF_SYSTEM_ID,
CONF_WIFI_SSID,
}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
simplisafe: SimpliSafe = hass.data[DOMAIN][entry.entry_id]
return async_redact_data(
{
"entry": {
"options": dict(entry.options),
},
"systems": [system.as_dict() for system in simplisafe.systems.values()],
},
TO_REDACT,
)

View File

@@ -3,7 +3,7 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==2021.12.2"],
"requirements": ["simplisafe-python==2022.01.0"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling",
"dhcp": [

View File

@@ -193,6 +193,7 @@ class SonosDiscoveryManager:
async def _async_stop_event_listener(self, event: Event | None = None) -> None:
for speaker in self.data.discovered.values():
speaker.activity_stats.log_report()
speaker.event_stats.log_report()
await asyncio.gather(
*(speaker.async_offline() for speaker in self.data.discovered.values())

View File

@@ -130,5 +130,6 @@ async def async_generate_speaker_info(
if s is speaker
}
payload["media"] = await async_generate_media_info(hass, speaker)
payload["activity_stats"] = speaker.activity_stats.report()
payload["event_stats"] = speaker.event_stats.report()
return payload

View File

@@ -9,6 +9,7 @@ import soco.config as soco_config
from soco.core import SoCo
from soco.exceptions import SoCoException
from homeassistant.components import persistent_notification
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity
@@ -22,6 +23,8 @@ from .const import (
)
from .speaker import SonosSpeaker
SUB_FAIL_URL = "https://www.home-assistant.io/integrations/sonos/#network-requirements"
_LOGGER = logging.getLogger(__name__)
@@ -70,10 +73,15 @@ class SonosEntity(Entity):
listener_msg = f"{self.speaker.subscription_address} (advertising as {soco_config.EVENT_ADVERTISE_IP})"
else:
listener_msg = self.speaker.subscription_address
_LOGGER.warning(
"%s cannot reach %s, falling back to polling, functionality may be limited",
self.speaker.zone_name,
listener_msg,
message = f"{self.speaker.zone_name} cannot reach {listener_msg}, falling back to polling, functionality may be limited"
log_link_msg = f", see {SUB_FAIL_URL} for more details"
notification_link_msg = f'.\n\nSee <a href="{SUB_FAIL_URL}">Sonos documentation</a> for more details.'
_LOGGER.warning(message + log_link_msg)
persistent_notification.async_create(
self.hass,
message + notification_link_msg,
"Sonos networking issue",
"sonos_subscriptions_failed",
)
self.speaker.subscriptions_failed = True
await self.speaker.async_unsubscribe()

View File

@@ -62,7 +62,7 @@ from .const import (
)
from .favorites import SonosFavorites
from .helpers import soco_error
from .statistics import EventStatistics
from .statistics import ActivityStatistics, EventStatistics
NEVER_TIME = -1200.0
EVENT_CHARGING = {
@@ -177,6 +177,7 @@ class SonosSpeaker:
self._event_dispatchers: dict[str, Callable] = {}
self._last_activity: float = NEVER_TIME
self._last_event_cache: dict[str, Any] = {}
self.activity_stats: ActivityStatistics = ActivityStatistics(self.zone_name)
self.event_stats: EventStatistics = EventStatistics(self.zone_name)
# Scheduled callback handles
@@ -528,6 +529,7 @@ class SonosSpeaker:
"""Track the last activity on this speaker, set availability and resubscribe."""
_LOGGER.debug("Activity on %s from %s", self.zone_name, source)
self._last_activity = time.monotonic()
self.activity_stats.activity(source, self._last_activity)
was_available = self.available
self.available = True
if not was_available:

View File

@@ -9,13 +9,49 @@ from soco.events_base import Event as SonosEvent, parse_event_xml
_LOGGER = logging.getLogger(__name__)
class EventStatistics:
class SonosStatistics:
"""Base class of Sonos statistics."""
def __init__(self, zone_name: str, kind: str) -> None:
"""Initialize SonosStatistics."""
self._stats = {}
self._stat_type = kind
self.zone_name = zone_name
def report(self) -> dict:
"""Generate a report for use in diagnostics."""
return self._stats.copy()
def log_report(self) -> None:
"""Log statistics for this speaker."""
_LOGGER.debug(
"%s statistics for %s: %s",
self._stat_type,
self.zone_name,
self.report(),
)
class ActivityStatistics(SonosStatistics):
"""Representation of Sonos activity statistics."""
def __init__(self, zone_name: str) -> None:
"""Initialize ActivityStatistics."""
super().__init__(zone_name, "Activity")
def activity(self, source: str, timestamp: float) -> None:
"""Track an activity occurrence."""
activity_entry = self._stats.setdefault(source, {"count": 0})
activity_entry["count"] += 1
activity_entry["last_seen"] = timestamp
class EventStatistics(SonosStatistics):
"""Representation of Sonos event statistics."""
def __init__(self, zone_name: str) -> None:
"""Initialize EventStatistics."""
self._stats = {}
self.zone_name = zone_name
super().__init__(zone_name, "Event")
def receive(self, event: SonosEvent) -> None:
"""Mark a received event by subscription type."""
@@ -38,11 +74,3 @@ class EventStatistics:
payload["soco:from_didl_string"] = from_didl_string.cache_info()
payload["soco:parse_event_xml"] = parse_event_xml.cache_info()
return payload
def log_report(self) -> None:
"""Log event statistics for this speaker."""
_LOGGER.debug(
"Event statistics for %s: %s",
self.zone_name,
self.report(),
)

View File

@@ -111,8 +111,6 @@ class TailscaleBinarySensorEntity(TailscaleEntity, BinarySensorEntity):
entity_description: TailscaleBinarySensorEntityDescription
@property
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Return the state of the sensor."""
return bool(
self.entity_description.is_on_fn(self.coordinator.data[self.device_id])
)
return self.entity_description.is_on_fn(self.coordinator.data[self.device_id])

View File

@@ -3,7 +3,7 @@
"name": "Tailscale",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tailscale",
"requirements": ["tailscale==0.1.6"],
"requirements": ["tailscale==0.2.0"],
"codeowners": ["@frenck"],
"quality_scale": "platinum",
"iot_class": "cloud_polling"

View File

@@ -96,7 +96,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device: SmartDevice = hass_data[entry.entry_id].device
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass_data.pop(entry.entry_id)
await device.protocol.close()
await device.protocol.close() # type: ignore
return unload_ok

View File

@@ -2,9 +2,9 @@
from __future__ import annotations
import logging
from typing import Any
from typing import Any, cast
from kasa import SmartDevice
from kasa import SmartBulb
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -41,7 +41,7 @@ async def async_setup_entry(
) -> None:
"""Set up switches."""
coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
device = coordinator.device
device = cast(SmartBulb, coordinator.device)
if device.is_bulb or device.is_light_strip or device.is_dimmer:
async_add_entities([TPLinkSmartBulb(device, coordinator)])
@@ -50,10 +50,11 @@ class TPLinkSmartBulb(CoordinatedTPLinkEntity, LightEntity):
"""Representation of a TPLink Smart Bulb."""
coordinator: TPLinkDataUpdateCoordinator
device: SmartBulb
def __init__(
self,
device: SmartDevice,
device: SmartBulb,
coordinator: TPLinkDataUpdateCoordinator,
) -> None:
"""Initialize the switch."""

View File

@@ -3,7 +3,7 @@
"name": "TP-Link Kasa Smart",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tplink",
"requirements": ["python-kasa==0.4.0"],
"requirements": ["python-kasa==0.4.1"],
"codeowners": ["@rytilahti", "@thegardenmonkey"],
"dependencies": ["network"],
"quality_scale": "platinum",
@@ -25,6 +25,10 @@
"hostname": "k[lp]*",
"macaddress": "403F8C*"
},
{
"hostname": "k[lp]*",
"macaddress": "C0C9E3*"
},
{
"hostname": "ep*",
"macaddress": "E848B8*"

View File

@@ -2,9 +2,9 @@
from __future__ import annotations
import logging
from typing import Any
from typing import Any, cast
from kasa import SmartDevice
from kasa import SmartDevice, SmartPlug
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
@@ -27,7 +27,7 @@ async def async_setup_entry(
) -> None:
"""Set up switches."""
coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
device = coordinator.device
device = cast(SmartPlug, coordinator.device)
if not device.is_plug and not device.is_strip:
return
entities: list = []
@@ -48,11 +48,12 @@ class SmartPlugLedSwitch(CoordinatedTPLinkEntity, SwitchEntity):
"""Representation of switch for the LED of a TPLink Smart Plug."""
coordinator: TPLinkDataUpdateCoordinator
device: SmartPlug
_attr_entity_category = EntityCategory.CONFIG
def __init__(
self, device: SmartDevice, coordinator: TPLinkDataUpdateCoordinator
self, device: SmartPlug, coordinator: TPLinkDataUpdateCoordinator
) -> None:
"""Initialize the LED switch."""
super().__init__(device, coordinator)

View File

@@ -137,7 +137,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
[
{
"code": self._speed.dpcode,
"value": self._speed.scale_value_back(percentage),
"value": int(self._speed.remap_value_from(percentage, 1, 100)),
}
]
)
@@ -178,7 +178,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
commands.append(
{
"code": self._speed.dpcode,
"value": int(self._speed.remap_value_from(percentage, 0, 100)),
"value": int(self._speed.remap_value_from(percentage, 1, 100)),
}
)
return
@@ -248,7 +248,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
if self._speed is not None:
if (value := self.device.status.get(self._speed.dpcode)) is None:
return None
return int(self._speed.remap_value_to(value, 0, 100))
return int(self._speed.remap_value_to(value, 1, 100))
if self._speeds is not None:
if (value := self.device.status.get(self._speeds.dpcode)) is None:

View File

@@ -7,14 +7,14 @@ from typing import Any
from homeassistant.components.diagnostics import REDACTED, async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import format_mac
from .const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN
TO_REDACT = {CONF_CONTROLLER, CONF_PASSWORD}
REDACT_CONFIG = {CONF_CONTROLLER, CONF_PASSWORD, CONF_USERNAME}
REDACT_CONFIG = {CONF_CONTROLLER, CONF_HOST, CONF_PASSWORD, CONF_USERNAME}
REDACT_CLIENTS = {"bssid", "essid"}
REDACT_DEVICES = {
"anon_id",
@@ -31,24 +31,42 @@ REDACT_WLANS = {"bc_filter_list", "x_passphrase"}
@callback
def async_replace_data(data: Mapping, to_replace: dict[str, str]) -> dict[str, Any]:
"""Replace sensitive data in a dict."""
if not isinstance(data, (Mapping, list, set, tuple)):
return to_replace.get(data, data)
def async_replace_dict_data(
data: Mapping, to_replace: dict[str, str]
) -> dict[str, Any]:
"""Redact sensitive data in a dict."""
redacted = {**data}
for key, value in redacted.items():
for key, value in data.items():
if isinstance(value, dict):
redacted[key] = async_replace_data(value, to_replace)
redacted[key] = async_replace_dict_data(value, to_replace)
elif isinstance(value, (list, set, tuple)):
redacted[key] = [async_replace_data(item, to_replace) for item in value]
redacted[key] = async_replace_list_data(value, to_replace)
elif isinstance(value, str):
if value in to_replace:
redacted[key] = to_replace[value]
elif value.count(":") == 5:
redacted[key] = REDACTED
return redacted
@callback
def async_replace_list_data(
data: list | set | tuple, to_replace: dict[str, str]
) -> list[Any]:
"""Redact sensitive data in a list."""
redacted = []
for item in data:
new_value = None
if isinstance(item, (list, set, tuple)):
new_value = async_replace_list_data(item, to_replace)
elif isinstance(item, Mapping):
new_value = async_replace_dict_data(item, to_replace)
elif isinstance(item, str):
if item in to_replace:
new_value = to_replace[item]
elif item.count(":") == 5:
new_value = REDACTED
redacted.append(new_value or item)
return redacted
@@ -73,26 +91,28 @@ async def async_get_config_entry_diagnostics(
counter += 1
diag["config"] = async_redact_data(
async_replace_data(config_entry.as_dict(), macs_to_redact), REDACT_CONFIG
async_replace_dict_data(config_entry.as_dict(), macs_to_redact), REDACT_CONFIG
)
diag["site_role"] = controller.site_role
diag["entities"] = async_replace_data(controller.entities, macs_to_redact)
diag["entities"] = async_replace_dict_data(controller.entities, macs_to_redact)
diag["clients"] = {
macs_to_redact[k]: async_redact_data(
async_replace_data(v.raw, macs_to_redact), REDACT_CLIENTS
async_replace_dict_data(v.raw, macs_to_redact), REDACT_CLIENTS
)
for k, v in controller.api.clients.items()
}
diag["devices"] = {
macs_to_redact[k]: async_redact_data(
async_replace_data(v.raw, macs_to_redact), REDACT_DEVICES
async_replace_dict_data(v.raw, macs_to_redact), REDACT_DEVICES
)
for k, v in controller.api.devices.items()
}
diag["dpi_apps"] = {k: v.raw for k, v in controller.api.dpi_apps.items()}
diag["dpi_groups"] = {k: v.raw for k, v in controller.api.dpi_groups.items()}
diag["wlans"] = {
k: async_redact_data(async_replace_data(v.raw, macs_to_redact), REDACT_WLANS)
k: async_redact_data(
async_replace_dict_data(v.raw, macs_to_redact), REDACT_WLANS
)
for k, v in controller.api.wlans.items()
}

View File

@@ -3,7 +3,7 @@ import logging
from typing import Any
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
@@ -78,34 +78,14 @@ class UniFiBase(Entity):
raise NotImplementedError
async def remove_item(self, keys: set) -> None:
"""Remove entity if key is part of set.
Remove entity if no entry in entity registry exist.
Remove entity registry entry if no entry in device registry exist.
Remove device registry entry if there is only one linked entity (this entity).
Remove config entry reference from device registry entry if there is more than one config entry.
Remove entity registry entry if there are more than one entity linked to the device registry entry.
"""
"""Remove entity if key is part of set."""
if self.key not in keys:
return
entity_registry = er.async_get(self.hass)
entity_entry = entity_registry.async_get(self.entity_id)
if not entity_entry:
if self.registry_entry:
er.async_get(self.hass).async_remove(self.entity_id)
else:
await self.async_remove(force_remove=True)
return
device_registry = dr.async_get(self.hass)
device_entry = device_registry.async_get(entity_entry.device_id)
if not device_entry:
entity_registry.async_remove(self.entity_id)
return
device_registry.async_update_device(
entity_entry.device_id,
remove_config_entry_id=self.controller.config_entry.entry_id,
)
entity_registry.async_remove(self.entity_id)
@property
def should_poll(self) -> bool:

View File

@@ -36,7 +36,7 @@ from .const import (
OUTDATED_LOG_MESSAGE,
)
from .discovery import async_start_discovery
from .utils import _async_short_mac, _async_unifi_mac_from_hass
from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass
_LOGGER = logging.getLogger(__name__)
@@ -88,32 +88,35 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
self._discovered_device = discovery_info
mac = _async_unifi_mac_from_hass(discovery_info["hw_addr"])
await self.async_set_unique_id(mac)
source_ip = discovery_info["source_ip"]
direct_connect_domain = discovery_info["direct_connect_domain"]
for entry in self._async_current_entries(include_ignore=False):
if entry.unique_id != mac:
continue
new_host = None
if (
_host_is_direct_connect(entry.data[CONF_HOST])
and discovery_info["direct_connect_domain"]
and entry.data[CONF_HOST] != discovery_info["direct_connect_domain"]
entry_host = entry.data[CONF_HOST]
entry_has_direct_connect = _host_is_direct_connect(entry_host)
if entry.unique_id == mac:
new_host = None
if (
entry_has_direct_connect
and direct_connect_domain
and entry_host != direct_connect_domain
):
new_host = direct_connect_domain
elif not entry_has_direct_connect and entry_host != source_ip:
new_host = source_ip
if new_host:
self.hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_HOST: new_host}
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="already_configured")
if entry_host in (direct_connect_domain, source_ip) or (
entry_has_direct_connect
and (ip := await _async_resolve(self.hass, entry_host))
and ip == source_ip
):
new_host = discovery_info["direct_connect_domain"]
elif (
not _host_is_direct_connect(entry.data[CONF_HOST])
and entry.data[CONF_HOST] != discovery_info["source_ip"]
):
new_host = discovery_info["source_ip"]
if new_host:
self.hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_HOST: new_host}
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="already_configured")
self._abort_if_unique_id_configured(
updates={CONF_HOST: discovery_info["source_ip"]}
)
return self.async_abort(reason="already_configured")
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(

View File

@@ -1,10 +1,12 @@
"""UniFi Protect Integration utils."""
from __future__ import annotations
import contextlib
from enum import Enum
import socket
from typing import Any
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
def get_nested_attr(obj: Any, attr: str) -> Any:
@@ -33,3 +35,19 @@ def _async_unifi_mac_from_hass(mac: str) -> str:
def _async_short_mac(mac: str) -> str:
"""Get the short mac address from the full mac."""
return _async_unifi_mac_from_hass(mac)[-6:]
async def _async_resolve(hass: HomeAssistant, host: str) -> str | None:
"""Resolve a hostname to an ip."""
with contextlib.suppress(OSError):
return next(
iter(
raw[0]
for family, _, _, _, raw in await hass.loop.getaddrinfo(
host, None, type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP
)
if family == socket.AF_INET
),
None,
)
return None

View File

@@ -24,6 +24,7 @@ from homeassistant.helpers.typing import ConfigType
from . import async_control_connect
from .const import CONF_SOURCES, DEFAULT_NAME, DOMAIN, WEBOSTV_EXCEPTIONS
from .helpers import async_get_sources
DATA_SCHEMA = vol.Schema(
{
@@ -178,11 +179,14 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
options_input = {CONF_SOURCES: user_input[CONF_SOURCES]}
return self.async_create_entry(title="", data=options_input)
# Get sources
sources = self.options.get(CONF_SOURCES, "")
sources_list = await async_get_sources(self.host, self.key)
if not sources_list:
errors["base"] = "cannot_retrieve"
sources = [s for s in self.options.get(CONF_SOURCES, []) if s in sources_list]
if not sources:
sources = sources_list
options_schema = vol.Schema(
{
vol.Optional(
@@ -195,16 +199,3 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
return self.async_show_form(
step_id="init", data_schema=options_schema, errors=errors
)
async def async_get_sources(host: str, key: str) -> list[str]:
"""Construct sources list."""
try:
client = await async_control_connect(host, key)
except WEBOSTV_EXCEPTIONS:
return []
return [
*(app["title"] for app in client.apps.values()),
*(app["label"] for app in client.inputs.values()),
]

View File

@@ -6,8 +6,8 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntry
from . import WebOsClientWrapper
from .const import DATA_CONFIG_ENTRY, DOMAIN
from . import WebOsClientWrapper, async_control_connect
from .const import DATA_CONFIG_ENTRY, DOMAIN, LIVE_TV_APP_ID, WEBOSTV_EXCEPTIONS
@callback
@@ -81,3 +81,29 @@ def async_get_client_wrapper_by_device_entry(
)
return wrapper
async def async_get_sources(host: str, key: str) -> list[str]:
"""Construct sources list."""
try:
client = await async_control_connect(host, key)
except WEBOSTV_EXCEPTIONS:
return []
sources = []
found_live_tv = False
for app in client.apps.values():
sources.append(app["title"])
if app["id"] == LIVE_TV_APP_ID:
found_live_tv = True
for source in client.inputs.values():
sources.append(source["label"])
if source["appId"] == LIVE_TV_APP_ID:
found_live_tv = True
if not found_live_tv:
sources.append("Live TV")
# Preserve order when filtering duplicates
return list(dict.fromkeys(sources))

View File

@@ -3,7 +3,7 @@
"name": "LG webOS Smart TV",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/webostv",
"requirements": ["aiowebostv==0.1.1", "sqlalchemy==1.4.27"],
"requirements": ["aiowebostv==0.1.2", "sqlalchemy==1.4.27"],
"codeowners": ["@bendavid", "@thecode"],
"ssdp": [{"st": "urn:lge-com:service:webos-second-screen:1"}],
"quality_scale": "platinum",

View File

@@ -87,7 +87,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = (
icon="mdi:account-star",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda domain: domain.admin if domain.admin else None,
value_fn=lambda domain: getattr(domain, "admin", None),
),
WhoisSensorEntityDescription(
key="creation_date",
@@ -123,7 +123,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = (
icon="mdi:account",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda domain: domain.owner if domain.owner else None,
value_fn=lambda domain: getattr(domain, "owner", None),
),
WhoisSensorEntityDescription(
key="registrant",
@@ -131,7 +131,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = (
icon="mdi:account-edit",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda domain: domain.registrant if domain.registrant else None,
value_fn=lambda domain: getattr(domain, "registrant", None),
),
WhoisSensorEntityDescription(
key="registrar",
@@ -147,7 +147,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = (
icon="mdi:store",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda domain: domain.reseller if domain.reseller else None,
value_fn=lambda domain: getattr(domain, "reseller", None),
),
)
@@ -190,7 +190,6 @@ async def async_setup_entry(
)
for description in SENSORS
],
update_before_add=True,
)
@@ -234,17 +233,20 @@ class WhoisSensorEntity(CoordinatorEntity, SensorEntity):
if self.coordinator.data is None:
return None
attrs = {
ATTR_EXPIRES: self.coordinator.data.expiration_date.isoformat(),
}
attrs = {}
if expiration_date := self.coordinator.data.expiration_date:
attrs[ATTR_EXPIRES] = expiration_date.isoformat()
if self.coordinator.data.name_servers:
attrs[ATTR_NAME_SERVERS] = " ".join(self.coordinator.data.name_servers)
if name_servers := self.coordinator.data.name_servers:
attrs[ATTR_NAME_SERVERS] = " ".join(name_servers)
if self.coordinator.data.last_updated:
attrs[ATTR_UPDATED] = self.coordinator.data.last_updated.isoformat()
if last_updated := self.coordinator.data.last_updated:
attrs[ATTR_UPDATED] = last_updated.isoformat()
if self.coordinator.data.registrar:
attrs[ATTR_REGISTRAR] = self.coordinator.data.registrar
if registrar := self.coordinator.data.registrar:
attrs[ATTR_REGISTRAR] = registrar
if not attrs:
return None
return attrs

View File

@@ -0,0 +1,48 @@
"""Diagnostics support for WLED."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import WLEDDataUpdateCoordinator
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
data = {
"info": async_redact_data(coordinator.data.info.__dict__, "wifi"),
"state": coordinator.data.state.__dict__,
"effects": {
effect.effect_id: effect.name for effect in coordinator.data.effects
},
"palettes": {
palette.palette_id: palette.name for palette in coordinator.data.palettes
},
"playlists": {
playlist.playlist_id: {
"name": playlist.name,
"repeat": playlist.repeat,
"shuffle": playlist.shuffle,
"end": playlist.end.preset_id if playlist.end else None,
}
for playlist in coordinator.data.playlists
},
"presets": {
preset.preset_id: {
"name": preset.name,
"quick_label": preset.quick_label,
"on": preset.on,
"transition": preset.transition,
}
for preset in coordinator.data.presets
},
}
return data

View File

@@ -3,7 +3,7 @@
"name": "WLED",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wled",
"requirements": ["wled==0.12.0"],
"requirements": ["wled==0.13.0"],
"zeroconf": ["_wled._tcp.local."],
"codeowners": ["@frenck"],
"quality_scale": "platinum",

View File

@@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.restore_state import RestoreEntity
from . import XiaomiDevice
from .const import DOMAIN, GATEWAYS_KEY
@@ -181,6 +182,11 @@ class XiaomiNatgasSensor(XiaomiBinarySensor):
attrs.update(super().extra_state_attributes)
return attrs
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
self._state = False
def parse_data(self, data, raw_data):
"""Parse data sent by gateway."""
if DENSITY in data:
@@ -232,6 +238,11 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
self._state = False
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
self._state = False
def parse_data(self, data, raw_data):
"""Parse data sent by gateway.
@@ -293,7 +304,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
return True
class XiaomiDoorSensor(XiaomiBinarySensor):
class XiaomiDoorSensor(XiaomiBinarySensor, RestoreEntity):
"""Representation of a XiaomiDoorSensor."""
def __init__(self, device, xiaomi_hub, config_entry):
@@ -319,6 +330,15 @@ class XiaomiDoorSensor(XiaomiBinarySensor):
attrs.update(super().extra_state_attributes)
return attrs
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
if state is None:
return
self._state = state.state == "on"
def parse_data(self, data, raw_data):
"""Parse data sent by gateway."""
self._should_poll = False
@@ -362,6 +382,11 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor):
config_entry,
)
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
self._state = False
def parse_data(self, data, raw_data):
"""Parse data sent by gateway."""
self._should_poll = False
@@ -400,6 +425,11 @@ class XiaomiSmokeSensor(XiaomiBinarySensor):
attrs.update(super().extra_state_attributes)
return attrs
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
self._state = False
def parse_data(self, data, raw_data):
"""Parse data sent by gateway."""
if DENSITY in data:

View File

@@ -2,7 +2,7 @@
"domain": "zeroconf",
"name": "Zero-configuration networking (zeroconf)",
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
"requirements": ["zeroconf==0.38.1"],
"requirements": ["zeroconf==0.38.3"],
"dependencies": ["network", "api"],
"codeowners": ["@bdraco"],
"quality_scale": "internal",

View File

@@ -1,7 +1,6 @@
"""Support for the definition of zones."""
from __future__ import annotations
from collections.abc import Callable
import logging
from typing import Any, cast
@@ -10,7 +9,6 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
ATTR_EDITABLE,
ATTR_GPS_ACCURACY,
ATTR_LATITUDE,
ATTR_LONGITUDE,
CONF_ICON,
@@ -29,7 +27,6 @@ from homeassistant.helpers import (
config_validation as cv,
entity,
entity_component,
event,
service,
storage,
)
@@ -287,10 +284,7 @@ class Zone(entity.Entity):
"""Initialize the zone."""
self._config = config
self.editable = True
self._attrs: dict | None = None
self._remove_listener: Callable[[], None] | None = None
self._generate_attrs()
self._persons_in_zone: set[str] = set()
@classmethod
def from_yaml(cls, config: dict) -> Zone:
@@ -301,9 +295,9 @@ class Zone(entity.Entity):
return zone
@property
def state(self) -> int:
def state(self) -> str:
"""Return the state property really does nothing for a zone."""
return len(self._persons_in_zone)
return "zoning"
@property
def name(self) -> str:
@@ -320,11 +314,6 @@ class Zone(entity.Entity):
"""Return the icon if any."""
return self._config.get(CONF_ICON)
@property
def extra_state_attributes(self) -> dict | None:
"""Return the state attributes of the zone."""
return self._attrs
@property
def should_poll(self) -> bool:
"""Zone does not poll."""
@@ -338,59 +327,10 @@ class Zone(entity.Entity):
self._generate_attrs()
self.async_write_ha_state()
@callback
def _person_state_change_listener(self, evt: Event) -> None:
person_entity_id = evt.data["entity_id"]
cur_count = len(self._persons_in_zone)
if (
(state := evt.data["new_state"])
and (latitude := state.attributes.get(ATTR_LATITUDE)) is not None
and (longitude := state.attributes.get(ATTR_LONGITUDE)) is not None
and (accuracy := state.attributes.get(ATTR_GPS_ACCURACY)) is not None
and (
zone_state := async_active_zone(
self.hass, latitude, longitude, accuracy
)
)
and zone_state.entity_id == self.entity_id
):
self._persons_in_zone.add(person_entity_id)
elif person_entity_id in self._persons_in_zone:
self._persons_in_zone.remove(person_entity_id)
if len(self._persons_in_zone) != cur_count:
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
person_domain = "person" # avoid circular import
persons = self.hass.states.async_entity_ids(person_domain)
for person in persons:
state = self.hass.states.get(person)
if (
state is None
or (latitude := state.attributes.get(ATTR_LATITUDE)) is None
or (longitude := state.attributes.get(ATTR_LONGITUDE)) is None
or (accuracy := state.attributes.get(ATTR_GPS_ACCURACY)) is None
):
continue
zone_state = async_active_zone(self.hass, latitude, longitude, accuracy)
if zone_state is not None and zone_state.entity_id == self.entity_id:
self._persons_in_zone.add(person)
self.async_on_remove(
event.async_track_state_change_filtered(
self.hass,
event.TrackStates(False, set(), {person_domain}),
self._person_state_change_listener,
).async_remove
)
@callback
def _generate_attrs(self) -> None:
"""Generate new attrs based on config."""
self._attrs = {
self._attr_extra_state_attributes = {
ATTR_LATITUDE: self._config[CONF_LATITUDE],
ATTR_LONGITUDE: self._config[CONF_LONGITUDE],
ATTR_RADIUS: self._config[CONF_RADIUS],

View File

@@ -7,7 +7,7 @@ from .backports.enum import StrEnum
MAJOR_VERSION: Final = 2022
MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "0b2"
PATCH_VERSION: Final = "0b6"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)

View File

@@ -46,6 +46,11 @@ DHCP = [
"hostname": "blink*",
"macaddress": "B85F98*"
},
{
"domain": "blink",
"hostname": "blink*",
"macaddress": "00037F*"
},
{
"domain": "broadlink",
"macaddress": "34EA34*"
@@ -201,6 +206,11 @@ DHCP = [
"domain": "nuki",
"hostname": "nuki_bridge_*"
},
{
"domain": "oncue",
"hostname": "kohlergen*",
"macaddress": "00146F*"
},
{
"domain": "overkiz",
"hostname": "gateway*",
@@ -250,6 +260,11 @@ DHCP = [
"hostname": "roomba-*",
"macaddress": "80A589*"
},
{
"domain": "roomba",
"hostname": "roomba-*",
"macaddress": "DCF505*"
},
{
"domain": "samsungtv",
"hostname": "tizen*"
@@ -392,6 +407,11 @@ DHCP = [
"hostname": "k[lp]*",
"macaddress": "403F8C*"
},
{
"domain": "tplink",
"hostname": "k[lp]*",
"macaddress": "C0C9E3*"
},
{
"domain": "tplink",
"hostname": "ep*",

View File

@@ -15,7 +15,7 @@ ciso8601==2.2.0
cryptography==35.0.0
emoji==1.6.3
hass-nabucasa==0.52.0
home-assistant-frontend==20220127.0
home-assistant-frontend==20220201.0
httpx==0.21.3
ifaddr==0.1.7
jinja2==3.0.3
@@ -33,7 +33,7 @@ typing-extensions>=3.10.0.2,<5.0
voluptuous-serialize==2.5.0
voluptuous==0.12.2
yarl==1.7.2
zeroconf==0.38.1
zeroconf==0.38.3
# Constrain pycryptodome to avoid vulnerability
# see https://github.com/home-assistant/core/pull/16238

Some files were not shown because too many files have changed in this diff Show More