Compare commits

...

100 Commits

Author SHA1 Message Date
Franck Nijhof
35f4220f4e Merge pull request #75758 from home-assistant/rc 2022-07-26 17:22:42 +02:00
Franck Nijhof
fc43ee772c Bumped version to 2022.7.7 2022-07-26 10:56:44 +02:00
Aaron Bach
d756936a4e Fix AssertionError in RainMachine (#75668) 2022-07-26 10:56:02 +02:00
mvn23
674a59f138 Update pyotgw to 2.0.1 (#75663) 2022-07-26 10:55:57 +02:00
Aaron Bach
9173aef1ef Revert SimpliSafe auth flow to the quasi-manual OAuth method from 2021.11.0 (#75641)
* Revert "Migrate SimpliSafe to new web-based authentication (#57212)"

This reverts commit bf7c99c1f8.

* Tests 100%

* Version bump

* Add manifest version for custom component testing

* Remove manifest version

* Code review

* Fix tests
2022-07-26 10:55:15 +02:00
uvjustin
0407fc4581 Round up for stream record lookback (#75580) 2022-07-26 10:50:36 +02:00
On Freund
ec4835ef04 Change monoprice config flow to sync (#75306) 2022-07-26 10:50:33 +02:00
Pawel
a3950937e0 Fix Epson wrong volume value (#75264) 2022-07-26 10:50:30 +02:00
Tom Schneider
58b7f9a032 Fix hvv departures authentication (#75146) 2022-07-26 10:50:25 +02:00
Franck Nijhof
cd0656bab0 Merge pull request #75528 from home-assistant/rc 2022-07-20 22:52:11 +02:00
Franck Nijhof
7402dc824e Bumped version to 2022.7.6 2022-07-20 21:49:42 +02:00
Shay Levy
67fc1ac40a Bump aioshelly to 2.0.1 (#75523) 2022-07-20 21:49:18 +02:00
Aaron Bach
e692d2e284 Fix incorrect Ambient PWS lightning strike sensor state classes (#75520) 2022-07-20 21:49:15 +02:00
J. Nick Koston
4ac7d68552 Fix failure to raise on bad YAML syntax from include files (#75510)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2022-07-20 21:49:11 +02:00
Klaas Schoute
e53a072e8a Fix - Forcast.solar issue on saving settings in options flow without api key (#75504) 2022-07-20 21:49:08 +02:00
starkillerOG
787f55e513 Fix Netgear update entity (#75496) 2022-07-20 21:49:04 +02:00
Pascal Winters
0a11a623a5 Bump pySwitchbot to 0.14.1 (#75487) 2022-07-20 21:49:01 +02:00
Raman Gupta
55ef33af26 Bump pytomorrowio to 0.3.4 (#75478) 2022-07-20 21:48:57 +02:00
mkmer
5c2ef50fca Bump AIOAladdinConnect to 0.1.27 (#75400) 2022-07-20 21:48:06 +02:00
J. Nick Koston
630f731020 Fix HKC device triggers (#75371) 2022-07-20 21:42:42 +02:00
Aaron Bach
219d1a8a1e Handle (and better log) more AirVisual cloud API errors (#75332) 2022-07-20 21:42:38 +02:00
uvjustin
75641b6cd4 Apply filter to libav.hls logging namespace (#75330) 2022-07-20 21:42:35 +02:00
J. Nick Koston
340da786af Use default encoder when saving storage (#75319) 2022-07-20 21:42:32 +02:00
J. Nick Koston
7f43064f36 Use the orjson equivalent default encoder when save_json is passed the default encoder (#74377) 2022-07-20 21:42:26 +02:00
Nick Whyte
97b6912856 Upgrade ness_alarm dependencies (#75298)
* Upgrade ness alarm dependencies to fix #74571

* Update requirements
2022-07-20 21:35:26 +02:00
Aaron Bach
8b270cb487 Bump simplisafe-python to 2022.07.0 (#75294) 2022-07-20 21:35:23 +02:00
rikroe
8232a780eb Bump bimmer_connected to 0.10.1 (#75287)
Co-authored-by: rikroe <rikroe@users.noreply.github.com>
2022-07-20 21:35:19 +02:00
Khole
34c30f5ab9 Add fixes for hive light (#75286) 2022-07-20 21:35:15 +02:00
mvn23
75aea68b75 Update pyotgw to 2.0.0 (#75285)
* Update pyotgw to 2.0.0

* Include updated tests
2022-07-20 21:35:11 +02:00
Christopher Bailey
3a2beb2212 Improve UniFi Protect unauth handling (#75269) 2022-07-20 21:35:07 +02:00
clayton craft
bccdb29edc Bump venstarcolortouch to 0.18 (#75237)
venstarcolortouch: bump to 0.18
2022-07-20 21:35:03 +02:00
rikroe
8b4cf288e3 Force _attr_native_value to metric in bmw_connected_drive (#75225)
Co-authored-by: rikroe <rikroe@users.noreply.github.com>
2022-07-20 21:35:00 +02:00
apaperclip
1b61d72eaf Fix aruba ssh host key algorithm (#75224) 2022-07-20 21:34:56 +02:00
David F. Mulcahey
fdaaed6523 Fix ZHA light turn on issues (#75220)
* rename variable

* default transition is for color commands not level

* no extra command for groups

* don't transition color change when light off -> on

* clean up

* update condition

* fix condition again...

* simplify

* simplify

* missed one

* rename

* simplify

* rename

* tests

* color_provided_while_off with no changes

* fix missing flag clear

* more tests for transition scenarios

* add to comment

* fix comment

* don't transition when force on is set

* stale comment

* dont transition when colors don't change

* remove extra line

* remove debug print :)

* fix colors

* restore color to 65535 until investigated
2022-07-20 21:34:51 +02:00
Paulus Schoutsen
4e29bdf715 Merge pull request #75243 from home-assistant/rc 2022-07-14 22:46:51 -07:00
Allen Porter
a23b427025 Remove nest mac prefix that matches cast devices (#75108) 2022-07-14 22:01:41 -07:00
Paulus Schoutsen
c185e636ed Bumped version to 2022.7.5 2022-07-14 21:31:07 -07:00
Nathan Spencer
99d39b203f Bump pylitterbot to 2022.7.0 (#75241) 2022-07-14 21:31:03 -07:00
mkmer
e4f1fa48d3 Bump AIOAladdinConnect to 0.1.25 (#75235)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-07-14 21:31:02 -07:00
Zack Barett
c56caca182 Bump frontend to 20220707.1 (#75232) 2022-07-14 21:30:41 -07:00
Khole
62d77a135b Fix Hive power unit of measurement (#75210) 2022-07-14 21:29:41 -07:00
puddly
b231eea0c6 Skip iso4217 version 1.10, which includes a broken __init__.pyi file (#75200) 2022-07-14 21:29:41 -07:00
puddly
5eaa15138c Bump zigpy from 0.47.2 to 0.47.3 (#75194) 2022-07-14 21:29:40 -07:00
mkmer
d401faac7c Bumped AIOAladdin Connect to 0.1.24 (#75182) 2022-07-14 21:29:39 -07:00
Christopher Bailey
674e02914b Bump version of pyunifiprotect to 4.0.10 (#75180) 2022-07-14 21:29:38 -07:00
uvjustin
326ffdcd49 Fix playback of hls cameras in stream (#75166) 2022-07-14 21:29:37 -07:00
Michał Huryn
b5e24048db Fix Blebox light scenes (#75106)
* Bug fix for light platform, when async_turn_on recieves multiple keys.

* Changes according to @MartinHjelmare suggestion.

* Moved effect set call in BleBoxLightEntity.async_turn_on method.

* Added tests for effect in light platform. Added ValueError raise if effect not in effect list.

* Removed duplicated line from test as @MartinHjelmare suggested.
2022-07-14 21:29:36 -07:00
Thomas Hollstegge
b1c07ac17a Fix Alexa: Only trigger doorbell event on actual state change to "ON" (#74924)
* Alexa: Only trigger doorbell event on actual state change to "ON"

* Remove unnecessary check for new_state

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

* First check state is `on` before checking the old state

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2022-07-14 21:29:35 -07:00
Michał Huryn
a5693c083f Address Blebox uniapi review sidenotes (#74298)
* Changes accordingly to sidenotes given by @MartinHjelmare in pull #73834.

* Mini version bump according to notes in pull #73834.

* Error message fix, test adjustment.
2022-07-14 21:29:35 -07:00
Paulus Schoutsen
60e170c863 Merge pull request #75147 from home-assistant/rc 2022-07-13 15:25:35 -07:00
Paulus Schoutsen
533ae85a49 Bumped version to 2022.7.4 2022-07-13 14:14:13 -07:00
Paulus Schoutsen
1555f706e5 Block bad pubnub version (#75138) 2022-07-13 14:14:01 -07:00
Aaron Bach
f052c3ca74 Ensure SimpliSafe diagnostics redact the code option (#75137) 2022-07-13 14:14:00 -07:00
puddly
89e87119f2 Bump ZHA dependencies (#75133) 2022-07-13 14:13:59 -07:00
kingy444
9e99ea68fb Fix Powerview top shade open position (#75110) 2022-07-13 14:13:58 -07:00
Joakim Plate
b105b0fbcb Make sure device tuple is a list on save (#75103) 2022-07-13 14:13:58 -07:00
Artem Draft
98c3bc56b5 Fix missing ordered states in universal media player (#75099) 2022-07-13 14:13:57 -07:00
Tom Harris
06e7f71891 Fix Insteon thermostat issues (#75079) 2022-07-13 14:13:56 -07:00
mkmer
55ae0228a9 Bump AIOAladdinConnect to 0.1.23 (#75065) 2022-07-13 14:13:55 -07:00
hahn-th
5c882429d4 Bump homematicip to 1.0.4 (#75053) 2022-07-13 14:13:54 -07:00
Thijs W
51a4c98562 Bump afsapi to 0.2.6 (#75041) 2022-07-13 14:13:53 -07:00
Gabe Cook
620d2ed8fd Fix Ruckus Unleashed SSH connection failures (#75032) 2022-07-13 14:13:52 -07:00
Ville Skyttä
bdc4171e37 Upgrade huawei-lte-api to 1.6.1 (#75030) 2022-07-13 14:13:51 -07:00
Phil Bruckner
f738d39ad9 Do not spam log when Life360 member location is missing (#75029) 2022-07-13 14:13:50 -07:00
Franck Nijhof
a34e72f3a1 Fix mix of aiohttp and requests in ClickSend TTS (#74985) 2022-07-13 14:13:50 -07:00
J. Nick Koston
c45313e9de JSON serialize NamedTuple subclasses with aiohttp (#74971) 2022-07-13 14:13:49 -07:00
Markus
4d81d056da Fix Pyload request content type headers (#74957) 2022-07-13 14:13:48 -07:00
henryptung
3f53022b50 Remove pip --prefix workaround (#74922)
Remove --prefix workaround

See discussion in https://github.com/home-assistant/core/issues/74405.

This workaround is no longer needed on pip >= 21.0 and actively causes problems for pip >= 21.3.
2022-07-13 14:13:47 -07:00
Alexei Chetroi
d31a0c8dca Correctly handle device triggers for missing ZHA devices (#74894) 2022-07-13 14:13:46 -07:00
Ludovico de Nittis
cc79c3d6e1 Update pyialarm to 2.2.0 (#74874) 2022-07-13 14:13:45 -07:00
Erik Montnemery
5e7174a5f4 Migrate homematicip_cloud to native_* (#74385) 2022-07-13 14:13:45 -07:00
Erik Montnemery
8259ce9868 Migrate ecobee to native_* (#74043) 2022-07-13 14:13:44 -07:00
Paulus Schoutsen
4418e6c4b6 Merge pull request #74926 from home-assistant/rc 2022-07-10 14:33:08 -07:00
Paulus Schoutsen
02452c7632 Bumped version to 2022.7.3 2022-07-10 13:25:47 -07:00
Thijs W
20f77ef832 Bump afsapi to 0.2.5 (#74907) 2022-07-10 13:25:43 -07:00
David Straub
adbcd8adb4 Bump pysml to 0.0.8 (fixes #74382) (#74875) 2022-07-10 13:25:42 -07:00
Hans Oischinger
bd069966f2 Fix Vicare One Time Charge (#74872) 2022-07-10 13:25:41 -07:00
Brandon Rothweiler
986a86ebed Bump pymazda to 0.3.6 (#74863) 2022-07-10 13:25:41 -07:00
Chris Talkington
01eae3687a Bump rokuecp to 0.17.0 (#74862) 2022-07-10 13:25:40 -07:00
Stephan Uhle
2f570fa715 Fixed unit of measurement. #70121 (#74838) 2022-07-10 13:25:39 -07:00
Aaron Bach
2ba285b8e5 Bump regenmaschine to 2022.07.1 (#74815) 2022-07-10 13:25:38 -07:00
Robert Svensson
07f4efcd83 Bump deCONZ dependency to fix #74791 (#74804) 2022-07-10 13:25:38 -07:00
Paul Annekov
357fe2a722 Bump python-gammu to 3.2.4 with Python 3.10 support (#74797) 2022-07-10 13:25:37 -07:00
Álvaro Fernández Rojas
2accc4c07d Update aioqsw to v0.1.1 (#74784) 2022-07-10 13:25:36 -07:00
Regev Brody
79b4f8ce0c Bump pyezviz to 0.2.0.9 (#74755)
* Bump ezviz dependency to fix #74618

* Bump ezviz dependency to fix #74618

Co-authored-by: J. Nick Koston <nick@koston.org>
2022-07-10 13:25:36 -07:00
Pieter Mulder
e233024533 Update pyCEC to version 0.5.2 (#74742) 2022-07-10 13:25:35 -07:00
Ethan Madden
43527d8d19 air_quality and filter_life fixes for Pur131S (#74740) 2022-07-10 13:25:34 -07:00
Aidan Timson
59471a6fbd Update systembridgeconnector to 3.3.2 (#74701) 2022-07-10 13:25:33 -07:00
kpine
f5d18108d0 Fix KeyError from zwave_js diagnostics (#74579) 2022-07-10 13:25:33 -07:00
Hans Oischinger
267057c989 Fix Vicare One Time Charge (#74872) 2022-07-10 13:15:43 -07:00
Paulus Schoutsen
5080246fb6 Merge pull request #74760 from home-assistant/rc 2022-07-08 16:08:11 -07:00
Shay Levy
a3abe7456e Fix CI failure due to integrations leaving dirty known_devices.yaml (#74329) 2022-07-08 14:29:24 -07:00
Paulus Schoutsen
14c6b8d41f Bumped version to 2022.7.2 2022-07-08 14:22:45 -07:00
Benoit Anastay
ea709912d4 Fix error with HDD temperature report in Freebox integration (#74718)
* Fix error whith HDD temperature report

There was a non handled error case, documented in issue https://github.com/home-assistant/core/issues/43812 back in 2020 and the fix wasn't applied

* Use get method instead of ignoring the sensor

* Update test values

Add idle state drive with unkown temp

* update Tests for system sensors api

* Fix booleans values

* Fix disk unique_id

There was a typo in the code
2022-07-08 14:22:39 -07:00
Aaron Bach
cb5658d7dc Bump regenmaschine to 2022.07.0 (#74680) 2022-07-08 14:22:38 -07:00
Paulus Schoutsen
7b1cad223d Bump atomicwrites (#74758) 2022-07-08 14:18:19 -07:00
Robert Svensson
e80fd4fc78 Bump deconz dependency to fix #74523 (#74710) 2022-07-08 14:18:18 -07:00
TheJulianJES
88d723736f Fix ZHA group not setting the correct color mode (#74687)
* Fix ZHA group not setting the correct color mode

* Changed to use _attr_color_mode
2022-07-08 14:18:18 -07:00
siyuan-nz
dc33d5db82 Add ssh-rsa as acceptable an host key algorithm (#74684) 2022-07-08 14:18:17 -07:00
Kevin Stillhammer
7ffc60fb2c Add missing strings for here_travel_time (#74641)
* Add missing strings for here_travel_time

* script.translations develop

* Correct origin_menu option
2022-07-08 14:18:16 -07:00
142 changed files with 2164 additions and 1021 deletions

View File

@@ -129,8 +129,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/binary_sensor/ @home-assistant/core
/tests/components/binary_sensor/ @home-assistant/core
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
/homeassistant/components/blebox/ @bbx-a @bbx-jp @riokuu
/tests/components/blebox/ @bbx-a @bbx-jp @riokuu
/homeassistant/components/blebox/ @bbx-a @riokuu
/tests/components/blebox/ @bbx-a @riokuu
/homeassistant/components/blink/ @fronzbot
/tests/components/blink/ @fronzbot
/homeassistant/components/blueprint/ @home-assistant/core

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Mapping
from datetime import timedelta
from math import ceil
from typing import Any, cast
from typing import Any
from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import (
@@ -12,6 +12,7 @@ from pyairvisual.errors import (
InvalidKeyError,
KeyExpiredError,
NodeProError,
UnauthorizedError,
)
from homeassistant.config_entries import ConfigEntry
@@ -210,9 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
try:
data = await api_coro
return cast(dict[str, Any], data)
except (InvalidKeyError, KeyExpiredError) as ex:
return await api_coro
except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex:
raise ConfigEntryAuthFailed from ex
except AirVisualError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err
@@ -253,8 +253,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async with NodeSamba(
entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD]
) as node:
data = await node.async_get_latest_measurements()
return cast(dict[str, Any], data)
return await node.async_get_latest_measurements()
except NodeProError as err:
raise UpdateFailed(f"Error while retrieving data: {err}") from err

View File

@@ -9,8 +9,10 @@ from pyairvisual import CloudAPI, NodeSamba
from pyairvisual.errors import (
AirVisualError,
InvalidKeyError,
KeyExpiredError,
NodeProError,
NotFoundError,
UnauthorizedError,
)
import voluptuous as vol
@@ -119,7 +121,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if user_input[CONF_API_KEY] not in valid_keys:
try:
await coro
except InvalidKeyError:
except (InvalidKeyError, KeyExpiredError, UnauthorizedError):
errors[CONF_API_KEY] = "invalid_api_key"
except NotFoundError:
errors[CONF_CITY] = "location_not_found"

View File

@@ -3,7 +3,7 @@
"name": "AirVisual",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airvisual",
"requirements": ["pyairvisual==5.0.9"],
"requirements": ["pyairvisual==2022.07.0"],
"codeowners": ["@bachya"],
"iot_class": "cloud_polling",
"loggers": ["pyairvisual", "pysmb"]

View File

@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .const import CLIENT_ID, DOMAIN
_LOGGER: Final = logging.getLogger(__name__)
@@ -23,7 +23,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up platform from a ConfigEntry."""
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
acc = AladdinConnectClient(username, password, async_get_clientsession(hass))
acc = AladdinConnectClient(
username, password, async_get_clientsession(hass), CLIENT_ID
)
try:
if not await acc.login():
raise ConfigEntryAuthFailed("Incorrect Password")

View File

@@ -18,7 +18,7 @@ from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .const import CLIENT_ID, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -38,7 +38,10 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
acc = AladdinConnectClient(
data[CONF_USERNAME], data[CONF_PASSWORD], async_get_clientsession(hass)
data[CONF_USERNAME],
data[CONF_PASSWORD],
async_get_clientsession(hass),
CLIENT_ID,
)
login = await acc.login()
await acc.close()

View File

@@ -18,3 +18,4 @@ STATES_MAP: Final[dict[str, str]] = {
DOMAIN = "aladdin_connect"
SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
CLIENT_ID = "1000"

View File

@@ -88,6 +88,7 @@ class AladdinDevice(CoverEntity):
self._device_id = device["device_id"]
self._number = device["door_number"]
self._attr_name = device["name"]
self._serial = device["serial"]
self._attr_unique_id = f"{self._device_id}-{self._number}"
async def async_added_to_hass(self) -> None:
@@ -97,8 +98,8 @@ class AladdinDevice(CoverEntity):
"""Schedule a state update."""
self.async_write_ha_state()
self._acc.register_callback(update_callback, self._number)
await self._acc.get_doors(self._number)
self._acc.register_callback(update_callback, self._serial)
await self._acc.get_doors(self._serial)
async def async_will_remove_from_hass(self) -> None:
"""Close Aladdin Connect before removing."""
@@ -114,7 +115,7 @@ class AladdinDevice(CoverEntity):
async def async_update(self) -> None:
"""Update status of cover."""
await self._acc.get_doors(self._number)
await self._acc.get_doors(self._serial)
@property
def is_closed(self) -> bool | None:

View File

@@ -2,7 +2,7 @@
"domain": "aladdin_connect",
"name": "Aladdin Connect",
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"requirements": ["AIOAladdinConnect==0.1.21"],
"requirements": ["AIOAladdinConnect==0.1.27"],
"codeowners": ["@mkmer"],
"iot_class": "cloud_polling",
"loggers": ["aladdin_connect"],

View File

@@ -11,3 +11,4 @@ class DoorDevice(TypedDict):
door_number: int
name: str
status: str
serial: str

View File

@@ -86,7 +86,9 @@ async def async_enable_proactive_mode(hass, smart_home_config):
return
if should_doorbell:
if new_state.state == STATE_ON:
if new_state.state == STATE_ON and (
old_state is None or old_state.state != STATE_ON
):
await async_send_doorbell_event_message(
hass, smart_home_config, alexa_changed_entity
)

View File

@@ -281,14 +281,14 @@ SENSOR_DESCRIPTIONS = (
name="Lightning Strikes Per Day",
icon="mdi:lightning-bolt",
native_unit_of_measurement="strikes",
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key=TYPE_LIGHTNING_PER_HOUR,
name="Lightning Strikes Per Hour",
icon="mdi:lightning-bolt",
native_unit_of_measurement="strikes",
state_class=SensorStateClass.TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
key=TYPE_MAXDAILYGUST,

View File

@@ -87,7 +87,7 @@ class ArubaDeviceScanner(DeviceScanner):
def get_aruba_data(self):
"""Retrieve data from Aruba Access Point and return parsed result."""
connect = f"ssh {self.username}@{self.host}"
connect = f"ssh {self.username}@{self.host} -o HostKeyAlgorithms=ssh-rsa"
ssh = pexpect.spawn(connect)
query = ssh.expect(
[

View File

@@ -3,13 +3,7 @@
from homeassistant.components.cover import CoverDeviceClass
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.switch import SwitchDeviceClass
from homeassistant.const import (
STATE_CLOSED,
STATE_CLOSING,
STATE_OPEN,
STATE_OPENING,
TEMP_CELSIUS,
)
from homeassistant.const import TEMP_CELSIUS
DOMAIN = "blebox"
PRODUCT = "product"
@@ -30,19 +24,6 @@ BLEBOX_TO_HASS_DEVICE_CLASSES = {
"temperature": SensorDeviceClass.TEMPERATURE,
}
BLEBOX_TO_HASS_COVER_STATES = {
None: None,
0: STATE_CLOSING, # moving down
1: STATE_OPENING, # moving up
2: STATE_OPEN, # manually stopped
3: STATE_CLOSED, # lower limit
4: STATE_OPEN, # upper limit / open
# gateController
5: STATE_OPEN, # overload
6: STATE_OPEN, # motor failure
# 7 is not used
8: STATE_OPEN, # safety stop
}
BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS}

View File

@@ -9,12 +9,26 @@ from homeassistant.components.cover import (
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING
from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import BleBoxEntity, create_blebox_entities
from .const import BLEBOX_TO_HASS_COVER_STATES, BLEBOX_TO_HASS_DEVICE_CLASSES
from .const import BLEBOX_TO_HASS_DEVICE_CLASSES
BLEBOX_TO_HASS_COVER_STATES = {
None: None,
0: STATE_CLOSING, # moving down
1: STATE_OPENING, # moving up
2: STATE_OPEN, # manually stopped
3: STATE_CLOSED, # lower limit
4: STATE_OPEN, # upper limit / open
# gateController
5: STATE_OPEN, # overload
6: STATE_OPEN, # motor failure
# 7 is not used
8: STATE_OPEN, # safety stop
}
async def async_setup_entry(

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from datetime import timedelta
import logging
from blebox_uniapi.error import BadOnValueError
import blebox_uniapi.light
from blebox_uniapi.light import BleboxColorMode
@@ -160,16 +159,21 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity):
else:
value = feature.apply_brightness(value, brightness)
try:
await self._feature.async_on(value)
except ValueError as exc:
raise ValueError(
f"Turning on '{self.name}' failed: Bad value {value}"
) from exc
if effect is not None:
effect_value = self.effect_list.index(effect)
await self._feature.async_api_command("effect", effect_value)
else:
try:
await self._feature.async_on(value)
except BadOnValueError as ex:
_LOGGER.error(
"Turning on '%s' failed: Bad value %s (%s)", self.name, value, ex
)
effect_value = self.effect_list.index(effect)
await self._feature.async_api_command("effect", effect_value)
except ValueError as exc:
raise ValueError(
f"Turning on with effect '{self.name}' failed: {effect} not in effect list."
) from exc
async def async_turn_off(self, **kwargs):
"""Turn the light off."""

View File

@@ -3,8 +3,8 @@
"name": "BleBox devices",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/blebox",
"requirements": ["blebox_uniapi==2.0.0"],
"codeowners": ["@bbx-a", "@bbx-jp", "@riokuu"],
"requirements": ["blebox_uniapi==2.0.1"],
"codeowners": ["@bbx-a", "@riokuu"],
"iot_class": "local_polling",
"loggers": ["blebox_uniapi"]
}

View File

@@ -33,7 +33,8 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator):
entry.data[CONF_PASSWORD],
get_region_from_name(entry.data[CONF_REGION]),
observer_position=GPSPosition(hass.config.latitude, hass.config.longitude),
use_metric_units=hass.config.units.is_metric,
# Force metric system as BMW API apparently only returns metric values now
use_metric_units=True,
)
self.read_only = entry.options[CONF_READ_ONLY]
self._entry = entry

View File

@@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": ["bimmer_connected==0.9.6"],
"requirements": ["bimmer_connected==0.10.1"],
"codeowners": ["@gerard33", "@rikroe"],
"config_flow": true,
"iot_class": "cloud_polling",

View File

@@ -16,7 +16,6 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_UNIT_SYSTEM_IMPERIAL,
LENGTH_KILOMETERS,
LENGTH_MILES,
PERCENTAGE,
@@ -183,10 +182,8 @@ class BMWSensor(BMWBaseEntity, SensorEntity):
self._attr_name = f"{vehicle.name} {description.key}"
self._attr_unique_id = f"{vehicle.vin}-{description.key}"
if unit_system.name == CONF_UNIT_SYSTEM_IMPERIAL:
self._attr_native_unit_of_measurement = description.unit_imperial
else:
self._attr_native_unit_of_measurement = description.unit_metric
# Force metric system as BMW API apparently only returns metric values now
self._attr_native_unit_of_measurement = description.unit_metric
@callback
def _handle_coordinator_update(self) -> None:

View File

@@ -3,7 +3,6 @@ from http import HTTPStatus
import json
import logging
from aiohttp.hdrs import CONTENT_TYPE
import requests
import voluptuous as vol
@@ -20,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
BASE_API_URL = "https://rest.clicksend.com/v3"
HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON}
HEADERS = {"Content-Type": CONTENT_TYPE_JSON}
CONF_LANGUAGE = "language"
CONF_VOICE = "voice"

View File

@@ -2,7 +2,7 @@
"domain": "cloud",
"name": "Home Assistant Cloud",
"documentation": "https://www.home-assistant.io/integrations/cloud",
"requirements": ["hass-nabucasa==0.54.0"],
"requirements": ["hass-nabucasa==0.54.1"],
"dependencies": ["http", "webhook"],
"after_dependencies": ["google_assistant", "alexa"],
"codeowners": ["@home-assistant/cloud"],

View File

@@ -3,7 +3,7 @@
"name": "deCONZ",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/deconz",
"requirements": ["pydeconz==96"],
"requirements": ["pydeconz==98"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics",

View File

@@ -7,20 +7,24 @@ from pyecobee.const import ECOBEE_STATE_UNKNOWN
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_TEMP_LOW,
ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED,
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_FAHRENHEIT
from homeassistant.const import (
LENGTH_METERS,
PRESSURE_HPA,
SPEED_METERS_PER_SECOND,
TEMP_FAHRENHEIT,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from homeassistant.util.pressure import convert as pressure_convert
from .const import (
DOMAIN,
@@ -49,6 +53,11 @@ async def async_setup_entry(
class EcobeeWeather(WeatherEntity):
"""Representation of Ecobee weather data."""
_attr_native_pressure_unit = PRESSURE_HPA
_attr_native_temperature_unit = TEMP_FAHRENHEIT
_attr_native_visibility_unit = LENGTH_METERS
_attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND
def __init__(self, data, name, index):
"""Initialize the Ecobee weather platform."""
self.data = data
@@ -101,7 +110,7 @@ class EcobeeWeather(WeatherEntity):
return None
@property
def temperature(self):
def native_temperature(self):
"""Return the temperature."""
try:
return float(self.get_forecast(0, "temperature")) / 10
@@ -109,18 +118,10 @@ class EcobeeWeather(WeatherEntity):
return None
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT
@property
def pressure(self):
def native_pressure(self):
"""Return the pressure."""
try:
pressure = self.get_forecast(0, "pressure")
if not self.hass.config.units.is_metric:
pressure = pressure_convert(pressure, PRESSURE_HPA, PRESSURE_INHG)
return round(pressure, 2)
return round(pressure)
except ValueError:
return None
@@ -134,15 +135,15 @@ class EcobeeWeather(WeatherEntity):
return None
@property
def visibility(self):
def native_visibility(self):
"""Return the visibility."""
try:
return int(self.get_forecast(0, "visibility")) / 1000
return int(self.get_forecast(0, "visibility"))
except ValueError:
return None
@property
def wind_speed(self):
def native_wind_speed(self):
"""Return the wind speed."""
try:
return int(self.get_forecast(0, "windSpeed"))
@@ -202,13 +203,13 @@ def _process_forecast(json):
json["weatherSymbol"]
]
if json["tempHigh"] != ECOBEE_STATE_UNKNOWN:
forecast[ATTR_FORECAST_TEMP] = float(json["tempHigh"]) / 10
forecast[ATTR_FORECAST_NATIVE_TEMP] = float(json["tempHigh"]) / 10
if json["tempLow"] != ECOBEE_STATE_UNKNOWN:
forecast[ATTR_FORECAST_TEMP_LOW] = float(json["tempLow"]) / 10
forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = float(json["tempLow"]) / 10
if json["windBearing"] != ECOBEE_STATE_UNKNOWN:
forecast[ATTR_FORECAST_WIND_BEARING] = int(json["windBearing"])
if json["windSpeed"] != ECOBEE_STATE_UNKNOWN:
forecast[ATTR_FORECAST_WIND_SPEED] = int(json["windSpeed"])
forecast[ATTR_FORECAST_NATIVE_WIND_SPEED] = int(json["windSpeed"])
except (ValueError, IndexError, KeyError):
return None

View File

@@ -2,7 +2,7 @@
"domain": "edl21",
"name": "EDL21",
"documentation": "https://www.home-assistant.io/integrations/edl21",
"requirements": ["pysml==0.0.7"],
"requirements": ["pysml==0.0.8"],
"codeowners": ["@mtdcr"],
"iot_class": "local_push",
"loggers": ["sml"]

View File

@@ -17,6 +17,7 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import (
CONF_NAME,
DEGREE,
ELECTRIC_CURRENT_AMPERE,
ELECTRIC_POTENTIAL_VOLT,
ENERGY_KILO_WATT_HOUR,
@@ -250,6 +251,7 @@ SENSOR_UNIT_MAPPING = {
"W": POWER_WATT,
"A": ELECTRIC_CURRENT_AMPERE,
"V": ELECTRIC_POTENTIAL_VOLT,
"°": DEGREE,
}
@@ -449,7 +451,7 @@ class EDL21Entity(SensorEntity):
@property
def native_unit_of_measurement(self):
"""Return the unit of measurement."""
if (unit := self._telegram.get("unit")) is None:
if (unit := self._telegram.get("unit")) is None or unit == 0:
return None
return SENSOR_UNIT_MAPPING[unit]

View File

@@ -3,7 +3,7 @@
"name": "Epson",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/epson",
"requirements": ["epson-projector==0.4.2"],
"requirements": ["epson-projector==0.4.6"],
"codeowners": ["@pszafer"],
"iot_class": "local_polling",
"loggers": ["epson_projector"]

View File

@@ -133,7 +133,10 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity):
self._source = SOURCE_LIST.get(source, self._source)
volume = await self._projector.get_property(VOLUME)
if volume:
self._volume = volume
try:
self._volume = float(volume)
except ValueError:
self._volume = None
elif power_state == BUSY:
self._state = STATE_ON
else:
@@ -176,11 +179,13 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity):
"""Turn on epson."""
if self._state == STATE_OFF:
await self._projector.send_command(TURN_ON)
self._state = STATE_ON
async def async_turn_off(self):
"""Turn off epson."""
if self._state == STATE_ON:
await self._projector.send_command(TURN_OFF)
self._state = STATE_OFF
@property
def source_list(self):

View File

@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/ezviz",
"dependencies": ["ffmpeg"],
"codeowners": ["@RenierM26", "@baqs"],
"requirements": ["pyezviz==0.2.0.8"],
"requirements": ["pyezviz==0.2.0.9"],
"config_flow": true,
"iot_class": "cloud_polling",
"loggers": ["paho_mqtt", "pyezviz"]

View File

@@ -99,7 +99,7 @@ class ForecastSolarOptionFlowHandler(OptionsFlow):
CONF_API_KEY,
description={
"suggested_value": self.config_entry.options.get(
CONF_API_KEY
CONF_API_KEY, ""
)
},
): str,

View File

@@ -113,7 +113,7 @@ class FreeboxRouter:
# According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree.
# Name and id of sensors may vary under Freebox devices.
for sensor in syst_datas["sensors"]:
self.sensors_temperature[sensor["name"]] = sensor["value"]
self.sensors_temperature[sensor["name"]] = sensor.get("value")
# Connection sensors
connection_datas: dict[str, Any] = await self._api.connection.get_status()

View File

@@ -159,7 +159,7 @@ class FreeboxDiskSensor(FreeboxSensor):
self._disk = disk
self._partition = partition
self._attr_name = f"{partition['label']} {description.name}"
self._unique_id = f"{self._router.mac} {description.key} {self._disk['id']} {self._partition['id']}"
self._attr_unique_id = f"{self._router.mac} {description.key} {self._disk['id']} {self._partition['id']}"
@property
def device_info(self) -> DeviceInfo:

View File

@@ -2,7 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20220707.0"],
"requirements": ["home-assistant-frontend==20220707.1"],
"dependencies": [
"api",
"auth",

View File

@@ -2,7 +2,7 @@
"domain": "frontier_silicon",
"name": "Frontier Silicon",
"documentation": "https://www.home-assistant.io/integrations/frontier_silicon",
"requirements": ["afsapi==0.2.4"],
"requirements": ["afsapi==0.2.6"],
"codeowners": ["@wlcrs"],
"iot_class": "local_polling"
}

View File

@@ -179,11 +179,14 @@ class AFSAPIDevice(MediaPlayerEntity):
self._attr_media_artist = await afsapi.get_play_artist()
self._attr_media_album_name = await afsapi.get_play_album()
self._attr_source = (await afsapi.get_mode()).label
radio_mode = await afsapi.get_mode()
self._attr_source = radio_mode.label if radio_mode is not None else None
self._attr_is_volume_muted = await afsapi.get_mute()
self._attr_media_image_url = await afsapi.get_play_graphic()
self._attr_sound_mode = (await afsapi.get_eq_preset()).label
eq_preset = await afsapi.get_eq_preset()
self._attr_sound_mode = eq_preset.label if eq_preset is not None else None
volume = await self.fs_device.get_volume()

View File

@@ -2,7 +2,7 @@
"domain": "hdmi_cec",
"name": "HDMI-CEC",
"documentation": "https://www.home-assistant.io/integrations/hdmi_cec",
"requirements": ["pyCEC==0.5.1"],
"requirements": ["pyCEC==0.5.2"],
"codeowners": [],
"iot_class": "local_push",
"loggers": ["pycec"]

View File

@@ -8,6 +8,13 @@
"mode": "Travel Mode"
}
},
"origin_menu": {
"title": "Choose Origin",
"menu_options": {
"origin_coordinates": "Using a map location",
"origin_entity": "Using an entity"
}
},
"origin_coordinates": {
"title": "Choose Origin",
"data": {

View File

@@ -39,6 +39,13 @@
},
"title": "Choose Origin"
},
"origin_menu": {
"menu_options": {
"origin_coordinates": "Using a map location",
"origin_entity": "Using an entity"
},
"title": "Choose Origin"
},
"user": {
"data": {
"api_key": "API Key",

View File

@@ -44,13 +44,15 @@ class HiveDeviceLight(HiveEntity, LightEntity):
super().__init__(hive, hive_device)
if self.device["hiveType"] == "warmwhitelight":
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
self._attr_color_mode = ColorMode.BRIGHTNESS
elif self.device["hiveType"] == "tuneablelight":
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP}
self._attr_color_mode = ColorMode.COLOR_TEMP
elif self.device["hiveType"] == "colourtuneablelight":
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
self._attr_min_mireds = self.device.get("min_mireds")
self._attr_max_mireds = self.device.get("max_mireds")
self._attr_min_mireds = 153
self._attr_max_mireds = 370
@refresh_system
async def async_turn_on(self, **kwargs):
@@ -94,6 +96,13 @@ class HiveDeviceLight(HiveEntity, LightEntity):
if self._attr_available:
self._attr_is_on = self.device["status"]["state"]
self._attr_brightness = self.device["status"]["brightness"]
if self.device["hiveType"] == "tuneablelight":
self._attr_color_temp = self.device["status"].get("color_temp")
if self.device["hiveType"] == "colourtuneablelight":
rgb = self.device["status"]["hs_color"]
self._attr_hs_color = color_util.color_RGB_to_hs(*rgb)
if self.device["status"]["mode"] == "COLOUR":
rgb = self.device["status"]["hs_color"]
self._attr_hs_color = color_util.color_RGB_to_hs(*rgb)
self._attr_color_mode = ColorMode.HS
else:
self._attr_color_temp = self.device["status"].get("color_temp")
self._attr_color_mode = ColorMode.COLOR_TEMP

View File

@@ -8,7 +8,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, POWER_KILO_WATT
from homeassistant.const import PERCENTAGE, POWER_WATT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -26,7 +26,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
),
SensorEntityDescription(
key="Power",
native_unit_of_measurement=POWER_KILO_WATT,
native_unit_of_measurement=POWER_WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),

View File

@@ -16,7 +16,7 @@ from homeassistant.components.automation import (
)
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS
@@ -86,13 +86,13 @@ class TriggerSource:
) -> CALLBACK_TYPE:
"""Attach a trigger."""
trigger_data = automation_info["trigger_data"]
job = HassJob(action)
@callback
def event_handler(char):
if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]:
return
self._hass.async_create_task(
action({"trigger": {**trigger_data, **config}})
)
self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}})
trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]]
iid = trigger["characteristic"]
@@ -231,11 +231,11 @@ async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry):
def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]):
"""Process events generated by a HomeKit accessory into automation triggers."""
trigger_sources: dict[str, TriggerSource] = conn.hass.data[TRIGGERS]
for (aid, iid), ev in events.items():
if aid in conn.devices:
device_id = conn.devices[aid]
if device_id in conn.hass.data[TRIGGERS]:
source = conn.hass.data[TRIGGERS][device_id]
if source := trigger_sources.get(device_id):
source.fire(iid, ev)

View File

@@ -3,7 +3,7 @@
"name": "HomematicIP Cloud",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"requirements": ["homematicip==1.0.3"],
"requirements": ["homematicip==1.0.4"],
"codeowners": [],
"quality_scale": "platinum",
"iot_class": "cloud_push",

View File

@@ -22,7 +22,7 @@ from homeassistant.components.weather import (
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TEMP_CELSIUS
from homeassistant.const import SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -71,6 +71,9 @@ async def async_setup_entry(
class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity):
"""Representation of the HomematicIP weather sensor plus & basic."""
_attr_native_temperature_unit = TEMP_CELSIUS
_attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR
def __init__(self, hap: HomematicipHAP, device) -> None:
"""Initialize the weather sensor."""
super().__init__(hap, device)
@@ -81,22 +84,17 @@ class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity):
return self._device.label
@property
def temperature(self) -> float:
def native_temperature(self) -> float:
"""Return the platform temperature."""
return self._device.actualTemperature
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def humidity(self) -> int:
"""Return the humidity."""
return self._device.humidity
@property
def wind_speed(self) -> float:
def native_wind_speed(self) -> float:
"""Return the wind speed."""
return self._device.windSpeed
@@ -129,6 +127,9 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor):
class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity):
"""Representation of the HomematicIP home weather."""
_attr_native_temperature_unit = TEMP_CELSIUS
_attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR
def __init__(self, hap: HomematicipHAP) -> None:
"""Initialize the home weather."""
hap.home.modelType = "HmIP-Home-Weather"
@@ -145,22 +146,17 @@ class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity):
return f"Weather {self._home.location.city}"
@property
def temperature(self) -> float:
def native_temperature(self) -> float:
"""Return the temperature."""
return self._device.weather.temperature
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def humidity(self) -> int:
"""Return the humidity."""
return self._device.weather.humidity
@property
def wind_speed(self) -> float:
def native_wind_speed(self) -> float:
"""Return the wind speed."""
return round(self._device.weather.windSpeed, 1)

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/huawei_lte",
"requirements": [
"huawei-lte-api==1.6.0",
"huawei-lte-api==1.6.1",
"stringcase==1.2.0",
"url-normalize==1.4.1"
],

View File

@@ -455,14 +455,6 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU):
super().__init__(coordinator, device_info, room_name, shade, name)
self._attr_unique_id = f"{self._shade.id}_top"
self._attr_name = f"{self._shade_name} Top"
# these shades share a class in parent API
# override open position for top shade
self._shade.open_position = {
ATTR_POSITION1: MIN_POSITION,
ATTR_POSITION2: MAX_POSITION,
ATTR_POSKIND1: POS_KIND_PRIMARY,
ATTR_POSKIND2: POS_KIND_SECONDARY,
}
@property
def should_poll(self) -> bool:
@@ -485,6 +477,21 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU):
# these need to be inverted to report state correctly in HA
return hd_position_to_hass(self.positions.secondary, MAX_POSITION)
@property
def open_position(self) -> PowerviewShadeMove:
"""Return the open position and required additional positions."""
# these shades share a class in parent API
# override open position for top shade
return PowerviewShadeMove(
{
ATTR_POSITION1: MIN_POSITION,
ATTR_POSITION2: MAX_POSITION,
ATTR_POSKIND1: POS_KIND_PRIMARY,
ATTR_POSKIND2: POS_KIND_SECONDARY,
},
{},
)
@callback
def _clamp_cover_limit(self, target_hass_position: int) -> int:
"""Dont allow a cover to go into an impossbile position."""

View File

@@ -3,7 +3,7 @@
"name": "HVV Departures",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hvv_departures",
"requirements": ["pygti==0.9.2"],
"requirements": ["pygti==0.9.3"],
"codeowners": ["@vigonotion"],
"iot_class": "cloud_polling",
"loggers": ["pygti"]

View File

@@ -2,7 +2,7 @@
"domain": "ialarm",
"name": "Antifurto365 iAlarm",
"documentation": "https://www.home-assistant.io/integrations/ialarm",
"requirements": ["pyialarm==1.9.0"],
"requirements": ["pyialarm==2.2.0"],
"codeowners": ["@RyuzakiKK"],
"config_flow": true,
"iot_class": "local_polling",

View File

@@ -86,7 +86,7 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity):
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
if self._insteon_device.properties[CELSIUS].value:
if self._insteon_device.configuration[CELSIUS].value:
return TEMP_CELSIUS
return TEMP_FAHRENHEIT

View File

@@ -4,8 +4,8 @@
"documentation": "https://www.home-assistant.io/integrations/insteon",
"dependencies": ["http", "websocket_api"],
"requirements": [
"pyinsteon==1.1.1",
"insteon-frontend-home-assistant==0.1.1"
"pyinsteon==1.1.3",
"insteon-frontend-home-assistant==0.2.0"
],
"codeowners": ["@teharris1"],
"dhcp": [

View File

@@ -37,7 +37,7 @@ from .const import (
SHOW_DRIVING,
SHOW_MOVING,
)
from .coordinator import Life360DataUpdateCoordinator
from .coordinator import Life360DataUpdateCoordinator, MissingLocReason
PLATFORMS = [Platform.DEVICE_TRACKER]
@@ -128,6 +128,10 @@ class IntegData:
coordinators: dict[str, Life360DataUpdateCoordinator] = field(
init=False, default_factory=dict
)
# member_id: missing location reason
missing_loc_reason: dict[str, MissingLocReason] = field(
init=False, default_factory=dict
)
# member_id: ConfigEntry.entry_id
tracked_members: dict[str, str] = field(init=False, default_factory=dict)
logged_circles: list[str] = field(init=False, default_factory=list)

View File

@@ -2,8 +2,10 @@
from __future__ import annotations
from contextlib import suppress
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any
from life360 import Life360, Life360Error, LoginError
@@ -33,6 +35,13 @@ from .const import (
)
class MissingLocReason(Enum):
"""Reason member location information is missing."""
VAGUE_ERROR_REASON = "vague error reason"
EXPLICIT_ERROR_REASON = "explicit error reason"
@dataclass
class Life360Place:
"""Life360 Place data."""
@@ -99,6 +108,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator):
max_retries=COMM_MAX_RETRIES,
authorization=entry.data[CONF_AUTHORIZATION],
)
self._missing_loc_reason = hass.data[DOMAIN].missing_loc_reason
async def _retrieve_data(self, func: str, *args: Any) -> list[dict[str, Any]]:
"""Get data from Life360."""
@@ -141,10 +151,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator):
if not int(member["features"]["shareLocation"]):
continue
# Note that member may be in more than one circle. If that's the case just
# go ahead and process the newly retrieved data (overwriting the older
# data), since it might be slightly newer than what was retrieved while
# processing another circle.
member_id = member["id"]
first = member["firstName"]
last = member["lastName"]
@@ -153,16 +160,45 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator):
else:
name = first or last
loc = member["location"]
if not loc:
if err_msg := member["issues"]["title"]:
if member["issues"]["dialog"]:
err_msg += f": {member['issues']['dialog']}"
else:
err_msg = "Location information missing"
LOGGER.error("%s: %s", name, err_msg)
cur_missing_reason = self._missing_loc_reason.get(member_id)
# Check if location information is missing. This can happen if server
# has not heard from member's device in a long time (e.g., has been off
# for a long time, or has lost service, etc.)
if loc := member["location"]:
with suppress(KeyError):
del self._missing_loc_reason[member_id]
else:
if explicit_reason := member["issues"]["title"]:
if extended_reason := member["issues"]["dialog"]:
explicit_reason += f": {extended_reason}"
# Note that different Circles can report missing location in
# different ways. E.g., one might report an explicit reason and
# another does not. If a vague reason has already been logged but a
# more explicit reason is now available, log that, too.
if (
cur_missing_reason is None
or cur_missing_reason == MissingLocReason.VAGUE_ERROR_REASON
and explicit_reason
):
if explicit_reason:
self._missing_loc_reason[
member_id
] = MissingLocReason.EXPLICIT_ERROR_REASON
err_msg = explicit_reason
else:
self._missing_loc_reason[
member_id
] = MissingLocReason.VAGUE_ERROR_REASON
err_msg = "Location information missing"
LOGGER.error("%s: %s", name, err_msg)
continue
# Note that member may be in more than one circle. If that's the case
# just go ahead and process the newly retrieved data (overwriting the
# older data), since it might be slightly newer than what was retrieved
# while processing another circle.
place = loc["name"] or None
if place:
@@ -179,7 +215,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator):
if self._hass.config.units.is_metric:
speed = convert(speed, LENGTH_MILES, LENGTH_KILOMETERS)
data.members[member["id"]] = Life360Member(
data.members[member_id] = Life360Member(
address,
dt_util.utc_from_timestamp(int(loc["since"])),
bool(int(loc["charge"])),

View File

@@ -3,7 +3,7 @@
"name": "Litter-Robot",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/litterrobot",
"requirements": ["pylitterbot==2022.3.0"],
"requirements": ["pylitterbot==2022.7.0"],
"codeowners": ["@natekspencer"],
"iot_class": "cloud_polling",
"loggers": ["pylitterbot"]

View File

@@ -3,7 +3,7 @@
"name": "Mazda Connected Services",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mazda",
"requirements": ["pymazda==0.3.3"],
"requirements": ["pymazda==0.3.6"],
"codeowners": ["@bdr99"],
"quality_scale": "platinum",
"iot_class": "cloud_polling",

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import logging
from pymonoprice import get_async_monoprice
from pymonoprice import get_monoprice
from serial import SerialException
import voluptuous as vol
@@ -56,7 +56,7 @@ async def validate_input(hass: core.HomeAssistant, data):
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
try:
await get_async_monoprice(data[CONF_PORT], hass.loop)
await hass.async_add_executor_job(get_monoprice, data[CONF_PORT])
except SerialException as err:
_LOGGER.error("Error connecting to Monoprice controller")
raise CannotConnect from err

View File

@@ -99,7 +99,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
client = Client(
host=host,
port=port,
loop=hass.loop,
update_interval=scan_interval.total_seconds(),
infer_arming_state=infer_arming_state,
)

View File

@@ -2,7 +2,7 @@
"domain": "ness_alarm",
"name": "Ness Alarm",
"documentation": "https://www.home-assistant.io/integrations/ness_alarm",
"requirements": ["nessclient==0.9.15"],
"requirements": ["nessclient==0.10.0"],
"codeowners": ["@nickw444"],
"iot_class": "local_push",
"loggers": ["nessclient"]

View File

@@ -11,8 +11,7 @@
"dhcp": [
{ "macaddress": "18B430*" },
{ "macaddress": "641666*" },
{ "macaddress": "D8EB46*" },
{ "macaddress": "1C53F9*" }
{ "macaddress": "D8EB46*" }
],
"iot_class": "cloud_push",
"loggers": ["google_nest_sdm", "nest"]

View File

@@ -59,7 +59,9 @@ class NetgearUpdateEntity(NetgearRouterEntity, UpdateEntity):
"""Latest version available for install."""
if self.coordinator.data is not None:
new_version = self.coordinator.data.get("NewVersion")
if new_version is not None:
if new_version is not None and not new_version.startswith(
self.installed_version
):
return new_version
return self.installed_version

View File

@@ -416,7 +416,7 @@ class OpenThermGatewayDevice:
self.status = {}
self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update"
self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update"
self.gateway = pyotgw.pyotgw()
self.gateway = pyotgw.OpenThermGateway()
self.gw_version = None
async def cleanup(self, event=None):
@@ -427,7 +427,7 @@ class OpenThermGatewayDevice:
async def connect_and_subscribe(self):
"""Connect to serial device and subscribe report handler."""
self.status = await self.gateway.connect(self.hass.loop, self.device_path)
self.status = await self.gateway.connect(self.device_path)
version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
self.gw_version = version_string[18:] if version_string else None
_LOGGER.debug(

View File

@@ -59,8 +59,8 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def test_connection():
"""Try to connect to the OpenTherm Gateway."""
otgw = pyotgw.pyotgw()
status = await otgw.connect(self.hass.loop, device)
otgw = pyotgw.OpenThermGateway()
status = await otgw.connect(device)
await otgw.disconnect()
return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)

View File

@@ -2,7 +2,7 @@
"domain": "opentherm_gw",
"name": "OpenTherm Gateway",
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
"requirements": ["pyotgw==1.1b1"],
"requirements": ["pyotgw==2.0.1"],
"codeowners": ["@mvn23"],
"config_flow": true,
"iot_class": "local_push",

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from datetime import timedelta
import logging
from aiohttp.hdrs import CONTENT_TYPE
import requests
import voluptuous as vol
@@ -144,7 +143,7 @@ class PyLoadAPI:
"""Initialize pyLoad API and set headers needed later."""
self.api_url = api_url
self.status = None
self.headers = {CONTENT_TYPE: CONTENT_TYPE_JSON}
self.headers = {"Content-Type": CONTENT_TYPE_JSON}
if username is not None and password is not None:
self.payload = {"username": username, "password": password}

View File

@@ -3,7 +3,7 @@
"name": "QNAP QSW",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
"requirements": ["aioqsw==0.1.0"],
"requirements": ["aioqsw==0.1.1"],
"codeowners": ["@Noltari"],
"iot_class": "local_polling",
"loggers": ["aioqsw"],

View File

@@ -183,7 +183,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_IP_ADDRESS],
entry.data[CONF_PASSWORD],
port=entry.data[CONF_PORT],
ssl=entry.data.get(CONF_SSL, DEFAULT_SSL),
use_ssl=entry.data.get(CONF_SSL, DEFAULT_SSL),
)
except RainMachineError as err:
raise ConfigEntryNotReady from err

View File

@@ -32,7 +32,7 @@ async def async_get_controller(
websession = aiohttp_client.async_get_clientsession(hass)
client = Client(session=websession)
try:
await client.load_local(ip_address, password, port=port, ssl=ssl)
await client.load_local(ip_address, password, port=port, use_ssl=ssl)
except RainMachineError:
return None
else:

View File

@@ -3,7 +3,7 @@
"name": "RainMachine",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
"requirements": ["regenmaschine==2022.06.1"],
"requirements": ["regenmaschine==2022.07.1"],
"codeowners": ["@bachya"],
"iot_class": "local_polling",
"homekit": {

View File

@@ -243,10 +243,8 @@ class TimeRemainingSensor(RainMachineEntity, RestoreSensor):
seconds_remaining = self.calculate_seconds_remaining()
new_timestamp = now + timedelta(seconds=seconds_remaining)
assert isinstance(self._attr_native_value, datetime)
if (
self._attr_native_value
isinstance(self._attr_native_value, datetime)
and new_timestamp - self._attr_native_value
< DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE
):

View File

@@ -199,7 +199,7 @@ class OptionsFlow(config_entries.OptionsFlow):
if not errors:
devices = {}
device = {
CONF_DEVICE_ID: device_id,
CONF_DEVICE_ID: list(device_id),
}
devices[self._selected_device_event_code] = device

View File

@@ -2,7 +2,7 @@
"domain": "roku",
"name": "Roku",
"documentation": "https://www.home-assistant.io/integrations/roku",
"requirements": ["rokuecp==0.16.0"],
"requirements": ["rokuecp==0.17.0"],
"homekit": {
"models": ["3820X", "3810X", "4660X", "7820X", "C105X", "C135X"]
},

View File

@@ -29,8 +29,7 @@ from .coordinator import RuckusUnleashedDataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Ruckus Unleashed from a config entry."""
try:
ruckus = await hass.async_add_executor_job(
Ruckus,
ruckus = await Ruckus.create(
entry.data[CONF_HOST],
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
@@ -42,10 +41,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
system_info = await hass.async_add_executor_job(ruckus.system_info)
system_info = await ruckus.system_info()
registry = device_registry.async_get(hass)
ap_info = await hass.async_add_executor_job(ruckus.ap_info)
ap_info = await ruckus.ap_info()
for device in ap_info[API_AP][API_ID].values():
registry.async_get_or_create(
config_entry_id=entry.entry_id,

View File

@@ -21,22 +21,24 @@ DATA_SCHEMA = vol.Schema(
)
def validate_input(hass: core.HomeAssistant, data):
async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
try:
ruckus = Ruckus(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD])
ruckus = await Ruckus.create(
data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]
)
except AuthenticationError as error:
raise InvalidAuth from error
except ConnectionError as error:
raise CannotConnect from error
mesh_name = ruckus.mesh_name()
mesh_name = await ruckus.mesh_name()
system_info = ruckus.system_info()
system_info = await ruckus.system_info()
try:
host_serial = system_info[API_SYSTEM_OVERVIEW][API_SERIAL]
except KeyError as error:
@@ -58,9 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors = {}
if user_input is not None:
try:
info = await self.hass.async_add_executor_job(
validate_input, self.hass, user_input
)
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:

View File

@@ -37,9 +37,7 @@ class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator):
async def _fetch_clients(self) -> dict:
"""Fetch clients from the API and format them."""
clients = await self.hass.async_add_executor_job(
self.ruckus.current_active_clients
)
clients = await self.ruckus.current_active_clients()
return {e[API_MAC]: e for e in clients[API_CURRENT_ACTIVE_CLIENTS][API_CLIENTS]}
async def _async_update_data(self) -> dict:

View File

@@ -3,7 +3,7 @@
"name": "Ruckus Unleashed",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed",
"requirements": ["pyruckus==0.12"],
"requirements": ["pyruckus==0.16"],
"codeowners": ["@gabe565"],
"iot_class": "local_polling",
"loggers": ["pexpect", "pyruckus"]

View File

@@ -3,7 +3,7 @@
"name": "Shelly",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==2.0.0"],
"requirements": ["aioshelly==2.0.1"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@@ -278,10 +278,8 @@ def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) ->
"""Bring a config entry up to current standards."""
if CONF_TOKEN not in entry.data:
raise ConfigEntryAuthFailed(
"New SimpliSafe OAuth standard requires re-authentication"
"SimpliSafe OAuth standard requires re-authentication"
)
if CONF_USERNAME not in entry.data:
raise ConfigEntryAuthFailed("Need to re-auth with username/password")
entry_updates = {}
if not entry.unique_id:

View File

@@ -1,48 +1,52 @@
"""Config flow to configure the SimpliSafe component."""
from __future__ import annotations
import asyncio
from collections.abc import Mapping
from typing import Any
from typing import Any, NamedTuple
import async_timeout
from simplipy import API
from simplipy.api import AuthStates
from simplipy.errors import InvalidCredentialsError, SimplipyError, Verify2FAPending
from simplipy.errors import InvalidCredentialsError, SimplipyError
from simplipy.util.auth import (
get_auth0_code_challenge,
get_auth0_code_verifier,
get_auth_url,
)
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_URL, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client, config_validation as cv
from .const import DOMAIN, LOGGER
DEFAULT_EMAIL_2FA_SLEEP = 3
DEFAULT_EMAIL_2FA_TIMEOUT = 600
STEP_REAUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): cv.string,
}
)
STEP_SMS_2FA_SCHEMA = vol.Schema(
{
vol.Required(CONF_CODE): cv.string,
}
)
CONF_AUTH_CODE = "auth_code"
STEP_USER_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_AUTH_CODE): cv.string,
}
)
class SimpliSafeOAuthValues(NamedTuple):
"""Define a named tuple to handle SimpliSafe OAuth strings."""
auth_url: str
code_verifier: str
@callback
def async_get_simplisafe_oauth_values() -> SimpliSafeOAuthValues:
"""Get a SimpliSafe OAuth code verifier and auth URL."""
code_verifier = get_auth0_code_verifier()
code_challenge = get_auth0_code_challenge(code_verifier)
auth_url = get_auth_url(code_challenge)
return SimpliSafeOAuthValues(auth_url, code_verifier)
class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a SimpliSafe config flow."""
@@ -50,45 +54,8 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
self._email_2fa_task: asyncio.Task | None = None
self._password: str | None = None
self._oauth_values: SimpliSafeOAuthValues = async_get_simplisafe_oauth_values()
self._reauth: bool = False
self._simplisafe: API | None = None
self._username: str | None = None
async def _async_authenticate(
self, originating_step_id: str, originating_step_schema: vol.Schema
) -> FlowResult:
"""Attempt to authenticate to the SimpliSafe API."""
assert self._password
assert self._username
errors = {}
session = aiohttp_client.async_get_clientsession(self.hass)
try:
self._simplisafe = await API.async_from_credentials(
self._username, self._password, session=session
)
except InvalidCredentialsError:
errors = {"base": "invalid_auth"}
except SimplipyError as err:
LOGGER.error("Unknown error while logging into SimpliSafe: %s", err)
errors = {"base": "unknown"}
if errors:
return self.async_show_form(
step_id=originating_step_id,
data_schema=originating_step_schema,
errors=errors,
description_placeholders={CONF_USERNAME: self._username},
)
assert self._simplisafe
if self._simplisafe.auth_state == AuthStates.PENDING_2FA_SMS:
return await self.async_step_sms_2fa()
return await self.async_step_email_2fa()
@staticmethod
@callback
@@ -98,146 +65,66 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Define the config flow to handle options."""
return SimpliSafeOptionsFlowHandler(config_entry)
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult:
"""Handle configuration by re-auth."""
self._reauth = True
if CONF_USERNAME not in entry_data:
# Old versions of the config flow may not have the username by this point;
# in that case, we reauth them by making them go through the user flow:
return await self.async_step_user()
self._username = entry_data[CONF_USERNAME]
return await self.async_step_reauth_confirm()
async def _async_get_email_2fa(self) -> None:
"""Define a task to wait for email-based 2FA."""
assert self._simplisafe
try:
async with async_timeout.timeout(DEFAULT_EMAIL_2FA_TIMEOUT):
while True:
try:
await self._simplisafe.async_verify_2fa_email()
except Verify2FAPending:
LOGGER.info("Email-based 2FA pending; trying again")
await asyncio.sleep(DEFAULT_EMAIL_2FA_SLEEP)
else:
break
finally:
self.hass.async_create_task(
self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
)
async def async_step_email_2fa(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle email-based two-factor authentication."""
if not self._email_2fa_task:
self._email_2fa_task = self.hass.async_create_task(
self._async_get_email_2fa()
)
return self.async_show_progress(
step_id="email_2fa", progress_action="email_2fa"
)
try:
await self._email_2fa_task
except asyncio.TimeoutError:
return self.async_show_progress_done(next_step_id="email_2fa_error")
return self.async_show_progress_done(next_step_id="finish")
async def async_step_email_2fa_error(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle an error during email-based two-factor authentication."""
return self.async_abort(reason="email_2fa_timed_out")
async def async_step_finish(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the final step."""
assert self._simplisafe
assert self._username
data = {
CONF_USERNAME: self._username,
CONF_TOKEN: self._simplisafe.refresh_token,
}
user_id = str(self._simplisafe.user_id)
if self._reauth:
# "Old" config entries utilized the user's email address (username) as the
# unique ID, whereas "new" config entries utilize the SimpliSafe user ID
# only one can exist at a time, but the presence of either one is a
# candidate for re-auth:
if existing_entries := [
entry
for entry in self.hass.config_entries.async_entries()
if entry.domain == DOMAIN
and entry.unique_id in (self._username, user_id)
]:
existing_entry = existing_entries[0]
self.hass.config_entries.async_update_entry(
existing_entry, unique_id=user_id, title=self._username, data=data
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(existing_entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
await self.async_set_unique_id(user_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=self._username, data=data)
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle re-auth completion."""
if not user_input:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_REAUTH_SCHEMA,
description_placeholders={CONF_USERNAME: self._username},
)
self._password = user_input[CONF_PASSWORD]
return await self._async_authenticate("reauth_confirm", STEP_REAUTH_SCHEMA)
async def async_step_sms_2fa(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle SMS-based two-factor authentication."""
if not user_input:
return self.async_show_form(
step_id="sms_2fa",
data_schema=STEP_SMS_2FA_SCHEMA,
)
assert self._simplisafe
try:
await self._simplisafe.async_verify_2fa_sms(user_input[CONF_CODE])
except InvalidCredentialsError:
return self.async_show_form(
step_id="sms_2fa",
data_schema=STEP_SMS_2FA_SCHEMA,
errors={CONF_CODE: "invalid_auth"},
)
return await self.async_step_finish()
return await self.async_step_user()
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the start of the config flow."""
if user_input is None:
return self.async_show_form(step_id="user", data_schema=STEP_USER_SCHEMA)
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_SCHEMA,
description_placeholders={CONF_URL: self._oauth_values.auth_url},
)
self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
return await self._async_authenticate("user", STEP_USER_SCHEMA)
errors = {}
session = aiohttp_client.async_get_clientsession(self.hass)
try:
simplisafe = await API.async_from_auth(
user_input[CONF_AUTH_CODE],
self._oauth_values.code_verifier,
session=session,
)
except InvalidCredentialsError:
errors = {"base": "invalid_auth"}
except SimplipyError as err:
LOGGER.error("Unknown error while logging into SimpliSafe: %s", err)
errors = {"base": "unknown"}
if errors:
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_SCHEMA,
errors=errors,
description_placeholders={CONF_URL: self._oauth_values.auth_url},
)
simplisafe_user_id = str(simplisafe.user_id)
data = {CONF_USERNAME: simplisafe_user_id, CONF_TOKEN: simplisafe.refresh_token}
if self._reauth:
existing_entry = await self.async_set_unique_id(simplisafe_user_id)
if not existing_entry:
# If we don't have an entry that matches this user ID, the user logged
# in with different credentials:
return self.async_abort(reason="wrong_account")
self.hass.config_entries.async_update_entry(
existing_entry, unique_id=simplisafe_user_id, data=data
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(existing_entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
await self.async_set_unique_id(simplisafe_user_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=simplisafe_user_id, data=data)
class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow):

View File

@@ -5,7 +5,7 @@ from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, CONF_LOCATION
from homeassistant.const import CONF_ADDRESS, CONF_CODE, CONF_LOCATION
from homeassistant.core import HomeAssistant
from . import SimpliSafe
@@ -23,6 +23,7 @@ CONF_WIFI_SSID = "wifi_ssid"
TO_REDACT = {
CONF_ADDRESS,
CONF_CODE,
CONF_CREDIT_CARD,
CONF_EXPIRES,
CONF_LOCATION,

View File

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

View File

@@ -1,38 +1,22 @@
{
"config": {
"step": {
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "Please re-enter the password for {username}.",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
},
"sms_2fa": {
"description": "Input the two-factor authentication code sent to you via SMS.",
"data": {
"code": "Code"
}
},
"user": {
"description": "Input your username and password.",
"description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL.",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
"auth_code": "Authorization Code"
}
}
},
"error": {
"identifier_exists": "Account already registered",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "This SimpliSafe account is already in use.",
"email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"progress": {
"email_2fa": "Check your email for a verification link from Simplisafe."
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"wrong_account": "The user credentials provided do not match this SimpliSafe account."
}
},
"options": {

View File

@@ -2,36 +2,20 @@
"config": {
"abort": {
"already_configured": "This SimpliSafe account is already in use.",
"email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.",
"reauth_successful": "Re-authentication was successful"
"reauth_successful": "Re-authentication was successful",
"wrong_account": "The user credentials provided do not match this SimpliSafe account."
},
"error": {
"identifier_exists": "Account already registered",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"progress": {
"email_2fa": "Check your email for a verification link from Simplisafe."
},
"step": {
"reauth_confirm": {
"data": {
"password": "Password"
},
"description": "Please re-enter the password for {username}.",
"title": "Reauthenticate Integration"
},
"sms_2fa": {
"data": {
"code": "Code"
},
"description": "Input the two-factor authentication code sent to you via SMS."
},
"user": {
"data": {
"password": "Password",
"username": "Username"
"auth_code": "Authorization Code"
},
"description": "Input your username and password."
"description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL."
}
}
},

View File

@@ -3,7 +3,7 @@
"name": "SMS notifications via GSM-modem",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sms",
"requirements": ["python-gammu==3.2.3"],
"requirements": ["python-gammu==3.2.4"],
"codeowners": ["@ocalvo"],
"iot_class": "local_polling",
"loggers": ["gammu"]

View File

@@ -57,9 +57,15 @@ from .const import (
SOURCE_TIMEOUT,
STREAM_RESTART_INCREMENT,
STREAM_RESTART_RESET_TIME,
TARGET_SEGMENT_DURATION_NON_LL_HLS,
)
from .core import PROVIDERS, IdleTimer, KeyFrameConverter, StreamOutput, StreamSettings
from .core import (
PROVIDERS,
STREAM_SETTINGS_NON_LL_HLS,
IdleTimer,
KeyFrameConverter,
StreamOutput,
StreamSettings,
)
from .diagnostics import Diagnostics
from .hls import HlsStreamOutput, async_setup_hls
@@ -181,14 +187,15 @@ def filter_libav_logging() -> None:
return logging.getLogger(__name__).isEnabledFor(logging.DEBUG)
for logging_namespace in (
"libav.mp4",
"libav.NULL",
"libav.h264",
"libav.hevc",
"libav.hls",
"libav.mp4",
"libav.mpegts",
"libav.rtsp",
"libav.tcp",
"libav.tls",
"libav.mpegts",
"libav.NULL",
):
logging.getLogger(logging_namespace).addFilter(libav_filter)
@@ -224,14 +231,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hls_part_timeout=2 * conf[CONF_PART_DURATION],
)
else:
hass.data[DOMAIN][ATTR_SETTINGS] = StreamSettings(
ll_hls=False,
min_segment_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS
- SEGMENT_DURATION_ADJUSTER,
part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS,
hls_advance_part_limit=3,
hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS,
)
hass.data[DOMAIN][ATTR_SETTINGS] = STREAM_SETTINGS_NON_LL_HLS
# Setup HLS
hls_endpoint = async_setup_hls(hass)
@@ -503,15 +503,16 @@ class Stream:
await self.start()
self._logger.debug("Started a stream recording of %s seconds", duration)
# Take advantage of lookback
hls: HlsStreamOutput = cast(HlsStreamOutput, self.outputs().get(HLS_PROVIDER))
if lookback > 0 and hls:
num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS)
if hls:
num_segments = min(int(lookback / hls.target_duration) + 1, MAX_SEGMENTS)
# Wait for latest segment, then add the lookback
await hls.recv()
recorder.prepend(list(hls.get_segments())[-num_segments - 1 : -1])
self._logger.debug("Started a stream recording of %s seconds", duration)
await recorder.async_record()
async def async_get_image(

View File

@@ -5,6 +5,7 @@ import asyncio
from collections import deque
from collections.abc import Callable, Coroutine, Iterable
import datetime
import logging
from typing import TYPE_CHECKING, Any
from aiohttp import web
@@ -16,13 +17,20 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.event import async_call_later
from homeassistant.util.decorator import Registry
from .const import ATTR_STREAMS, DOMAIN
from .const import (
ATTR_STREAMS,
DOMAIN,
SEGMENT_DURATION_ADJUSTER,
TARGET_SEGMENT_DURATION_NON_LL_HLS,
)
if TYPE_CHECKING:
from av import CodecContext, Packet
from . import Stream
_LOGGER = logging.getLogger(__name__)
PROVIDERS: Registry[str, type[StreamOutput]] = Registry()
@@ -37,6 +45,15 @@ class StreamSettings:
hls_part_timeout: float = attr.ib()
STREAM_SETTINGS_NON_LL_HLS = StreamSettings(
ll_hls=False,
min_segment_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS - SEGMENT_DURATION_ADJUSTER,
part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS,
hls_advance_part_limit=3,
hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS,
)
@attr.s(slots=True)
class Part:
"""Represent a segment part."""
@@ -426,12 +443,22 @@ class KeyFrameConverter:
return
packet = self.packet
self.packet = None
# decode packet (flush afterwards)
frames = self._codec_context.decode(packet)
for _i in range(2):
if frames:
for _ in range(2): # Retry once if codec context needs to be flushed
try:
# decode packet (flush afterwards)
frames = self._codec_context.decode(packet)
for _i in range(2):
if frames:
break
frames = self._codec_context.decode(None)
break
frames = self._codec_context.decode(None)
except EOFError:
_LOGGER.debug("Codec context needs flushing, attempting to reopen")
self._codec_context.close()
self._codec_context.open()
else:
_LOGGER.debug("Unable to decode keyframe")
return
if frames:
frame = frames[0]
if width and height:

View File

@@ -2,6 +2,10 @@
from __future__ import annotations
from collections.abc import Generator
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from io import BytesIO
def find_box(
@@ -135,3 +139,11 @@ def get_codec_string(mp4_bytes: bytes) -> str:
codecs.append(codec)
return ",".join(codecs)
def read_init(bytes_io: BytesIO) -> bytes:
"""Read the init from a mp4 file."""
bytes_io.seek(24)
moov_len = int.from_bytes(bytes_io.read(4), byteorder="big")
bytes_io.seek(0)
return bytes_io.read(24 + moov_len)

View File

@@ -5,11 +5,12 @@ from collections import defaultdict, deque
from collections.abc import Callable, Generator, Iterator, Mapping
import contextlib
import datetime
from io import BytesIO
from io import SEEK_END, BytesIO
import logging
from threading import Event
from typing import Any, cast
import attr
import av
from homeassistant.core import HomeAssistant
@@ -24,8 +25,16 @@ from .const import (
SEGMENT_CONTAINER_FORMAT,
SOURCE_TIMEOUT,
)
from .core import KeyFrameConverter, Part, Segment, StreamOutput, StreamSettings
from .core import (
STREAM_SETTINGS_NON_LL_HLS,
KeyFrameConverter,
Part,
Segment,
StreamOutput,
StreamSettings,
)
from .diagnostics import Diagnostics
from .fmp4utils import read_init
from .hls import HlsStreamOutput
_LOGGER = logging.getLogger(__name__)
@@ -108,7 +117,7 @@ class StreamMuxer:
hass: HomeAssistant,
video_stream: av.video.VideoStream,
audio_stream: av.audio.stream.AudioStream | None,
audio_bsf: av.BitStreamFilterContext | None,
audio_bsf: av.BitStreamFilter | None,
stream_state: StreamState,
stream_settings: StreamSettings,
) -> None:
@@ -120,6 +129,7 @@ class StreamMuxer:
self._input_video_stream: av.video.VideoStream = video_stream
self._input_audio_stream: av.audio.stream.AudioStream | None = audio_stream
self._audio_bsf = audio_bsf
self._audio_bsf_context: av.BitStreamFilterContext = None
self._output_video_stream: av.video.VideoStream = None
self._output_audio_stream: av.audio.stream.AudioStream | None = None
self._segment: Segment | None = None
@@ -151,7 +161,7 @@ class StreamMuxer:
**{
# Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970
# "cmaf" flag replaces several of the movflags used, but too recent to use for now
"movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer",
"movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov",
# Sometimes the first segment begins with negative timestamps, and this setting just
# adjusts the timestamps in the output from that segment to start from 0. Helps from
# having to make some adjustments in test_durations
@@ -164,7 +174,7 @@ class StreamMuxer:
# Fragment durations may exceed the 15% allowed variance but it seems ok
**(
{
"movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer",
"movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov",
# Create a fragment every TARGET_PART_DURATION. The data from each fragment is stored in
# a "Part" that can be combined with the data from all the other "Part"s, plus an init
# section, to reconstitute the data in a "Segment".
@@ -194,8 +204,11 @@ class StreamMuxer:
# Check if audio is requested
output_astream = None
if input_astream:
if self._audio_bsf:
self._audio_bsf_context = self._audio_bsf.create()
self._audio_bsf_context.set_input_stream(input_astream)
output_astream = container.add_stream(
template=self._audio_bsf or input_astream
template=self._audio_bsf_context or input_astream
)
return container, output_vstream, output_astream
@@ -238,15 +251,29 @@ class StreamMuxer:
self._part_has_keyframe |= packet.is_keyframe
elif packet.stream == self._input_audio_stream:
if self._audio_bsf:
self._audio_bsf.send(packet)
while packet := self._audio_bsf.recv():
if self._audio_bsf_context:
self._audio_bsf_context.send(packet)
while packet := self._audio_bsf_context.recv():
packet.stream = self._output_audio_stream
self._av_output.mux(packet)
return
packet.stream = self._output_audio_stream
self._av_output.mux(packet)
def create_segment(self) -> None:
"""Create a segment when the moov is ready."""
self._segment = Segment(
sequence=self._stream_state.sequence,
stream_id=self._stream_state.stream_id,
init=read_init(self._memory_file),
# Fetch the latest StreamOutputs, which may have changed since the
# worker started.
stream_outputs=self._stream_state.outputs,
start_time=self._start_time,
)
self._memory_file_pos = self._memory_file.tell()
self._memory_file.seek(0, SEEK_END)
def check_flush_part(self, packet: av.Packet) -> None:
"""Check for and mark a part segment boundary and record its duration."""
if self._memory_file_pos == self._memory_file.tell():
@@ -254,16 +281,10 @@ class StreamMuxer:
if self._segment is None:
# We have our first non-zero byte position. This means the init has just
# been written. Create a Segment and put it to the queue of each output.
self._segment = Segment(
sequence=self._stream_state.sequence,
stream_id=self._stream_state.stream_id,
init=self._memory_file.getvalue(),
# Fetch the latest StreamOutputs, which may have changed since the
# worker started.
stream_outputs=self._stream_state.outputs,
start_time=self._start_time,
)
self._memory_file_pos = self._memory_file.tell()
self.create_segment()
# When using delay_moov, the moov is not written until a moof is also ready
# Flush the moof
self.flush(packet, last_part=False)
else: # These are the ends of the part segments
self.flush(packet, last_part=False)
@@ -297,6 +318,10 @@ class StreamMuxer:
# Closing the av_output will write the remaining buffered data to the
# memory_file as a new moof/mdat.
self._av_output.close()
# With delay_moov, this may be the first time the file pointer has
# moved, so the segment may not yet have been created
if not self._segment:
self.create_segment()
elif not self._part_has_keyframe:
# Parts which are not the last part or an independent part should
# not have durations below 0.85 of the part target duration.
@@ -305,6 +330,9 @@ class StreamMuxer:
self._part_start_dts
+ 0.85 * self._stream_settings.part_target_duration / packet.time_base,
)
# Undo dts adjustments if we don't have ll_hls
if not self._stream_settings.ll_hls:
adjusted_dts = packet.dts
assert self._segment
self._memory_file.seek(self._memory_file_pos)
self._hass.loop.call_soon_threadsafe(
@@ -445,10 +473,7 @@ def get_audio_bitstream_filter(
_LOGGER.debug(
"ADTS AAC detected. Adding aac_adtstoaac bitstream filter"
)
bsf = av.BitStreamFilter("aac_adtstoasc")
bsf_context = bsf.create()
bsf_context.set_input_stream(audio_stream)
return bsf_context
return av.BitStreamFilter("aac_adtstoasc")
break
return None
@@ -489,7 +514,12 @@ def stream_worker(
audio_stream = None
# Disable ll-hls for hls inputs
if container.format.name == "hls":
stream_settings.ll_hls = False
for field in attr.fields(StreamSettings):
setattr(
stream_settings,
field.name,
getattr(STREAM_SETTINGS_NON_LL_HLS, field.name),
)
stream_state.diagnostics.set_value("container_format", container.format.name)
stream_state.diagnostics.set_value("video_codec", video_stream.name)
if audio_stream:

View File

@@ -2,7 +2,7 @@
"domain": "switchbot",
"name": "SwitchBot",
"documentation": "https://www.home-assistant.io/integrations/switchbot",
"requirements": ["PySwitchbot==0.14.0"],
"requirements": ["PySwitchbot==0.14.1"],
"config_flow": true,
"codeowners": ["@danielhiversen", "@RenierM26"],
"iot_class": "local_polling",

View File

@@ -93,7 +93,7 @@ class SystemBridgeDataUpdateCoordinator(
if not self.websocket_client.connected:
await self._setup_websocket()
await self.websocket_client.get_data(modules)
self.hass.async_create_task(self.websocket_client.get_data(modules))
async def async_handle_module(
self,
@@ -107,9 +107,7 @@ class SystemBridgeDataUpdateCoordinator(
async def _listen_for_data(self) -> None:
"""Listen for events from the WebSocket."""
try:
await self.websocket_client.register_data_listener(MODULES)
await self.websocket_client.listen(callback=self.async_handle_module)
except AuthenticationException as exception:
self.last_update_success = False
@@ -175,6 +173,9 @@ class SystemBridgeDataUpdateCoordinator(
self.async_update_listeners()
self.hass.async_create_task(self._listen_for_data())
await self.websocket_client.register_data_listener(MODULES)
self.last_update_success = True
self.async_update_listeners()

View File

@@ -3,7 +3,7 @@
"name": "System Bridge",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/system_bridge",
"requirements": ["systembridgeconnector==3.1.5"],
"requirements": ["systembridgeconnector==3.3.2"],
"codeowners": ["@timmo001"],
"zeroconf": ["_system-bridge._tcp.local."],
"after_dependencies": ["zeroconf"],

View File

@@ -3,7 +3,7 @@
"name": "Tomorrow.io",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tomorrowio",
"requirements": ["pytomorrowio==0.3.3"],
"requirements": ["pytomorrowio==0.3.4"],
"codeowners": ["@raman325", "@lymanepp"],
"iot_class": "cloud_polling"
}

View File

@@ -80,7 +80,7 @@ class UnifiDeviceScanner(DeviceScanner):
def _connect(self):
"""Connect to the Unifi AP SSH server."""
self.ssh = pxssh.pxssh()
self.ssh = pxssh.pxssh(options={"HostKeyAlgorithms": "ssh-rsa"})
try:
self.ssh.login(
self.host, self.username, password=self.password, port=self.port

View File

@@ -72,6 +72,7 @@ class ProtectData:
self._pending_camera_ids: set[str] = set()
self._unsub_interval: CALLBACK_TYPE | None = None
self._unsub_websocket: CALLBACK_TYPE | None = None
self._auth_failures = 0
self.last_update_success = False
self.api = protect
@@ -117,9 +118,13 @@ class ProtectData:
try:
updates = await self.api.update(force=force)
except NotAuthorized:
await self.async_stop()
_LOGGER.exception("Reauthentication required")
self._entry.async_start_reauth(self._hass)
if self._auth_failures < 10:
_LOGGER.exception("Auth error while updating")
self._auth_failures += 1
else:
await self.async_stop()
_LOGGER.exception("Reauthentication required")
self._entry.async_start_reauth(self._hass)
self.last_update_success = False
except ClientError:
if self.last_update_success:
@@ -129,6 +134,7 @@ class ProtectData:
self._async_process_updates(self.api.bootstrap)
else:
self.last_update_success = True
self._auth_failures = 0
self._async_process_updates(updates)
@callback

View File

@@ -3,7 +3,7 @@
"name": "UniFi Protect",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifiprotect",
"requirements": ["pyunifiprotect==4.0.9", "unifi-discovery==1.1.4"],
"requirements": ["pyunifiprotect==4.0.10", "unifi-discovery==1.1.4"],
"dependencies": ["http"],
"codeowners": ["@briis", "@AngellusMortis", "@bdraco"],
"quality_scale": "platinum",

View File

@@ -69,11 +69,13 @@ from homeassistant.const import (
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP,
STATE_BUFFERING,
STATE_IDLE,
STATE_OFF,
STATE_ON,
STATE_PAUSED,
STATE_PLAYING,
STATE_STANDBY,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
@@ -101,8 +103,10 @@ STATES_ORDER = [
STATE_UNAVAILABLE,
STATE_OFF,
STATE_IDLE,
STATE_STANDBY,
STATE_ON,
STATE_PAUSED,
STATE_BUFFERING,
STATE_PLAYING,
]
ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string)

View File

@@ -3,7 +3,7 @@
"name": "Venstar",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/venstar",
"requirements": ["venstarcolortouch==0.17"],
"requirements": ["venstarcolortouch==0.18"],
"codeowners": ["@garbled1"],
"iot_class": "local_polling",
"loggers": ["venstarcolortouch"]

View File

@@ -83,13 +83,12 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = (
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda device: device.details["filter_life"],
value_fn=lambda device: device.filter_life,
exists_fn=lambda device: sku_supported(device, FILTER_LIFE_SUPPORTED),
),
VeSyncSensorEntityDescription(
key="air-quality",
name="Air Quality",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda device: device.details["air_quality"],
exists_fn=lambda device: sku_supported(device, AIR_QUALITY_SUPPORTED),
),

View File

@@ -3,7 +3,7 @@
"name": "Viessmann ViCare",
"documentation": "https://www.home-assistant.io/integrations/vicare",
"codeowners": ["@oischinger"],
"requirements": ["PyViCare==2.16.2"],
"requirements": ["PyViCare==2.16.4"],
"iot_class": "cloud_polling",
"config_flow": true,
"dhcp": [

View File

@@ -170,7 +170,7 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice:
device_registry = dr.async_get(hass)
registry_device = device_registry.async_get(device_id)
if not registry_device:
raise ValueError(f"Device id `{device_id}` not found in registry.")
raise KeyError(f"Device id `{device_id}` not found in registry.")
zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
ieee_address = list(list(registry_device.identifiers)[0])[1]
ieee = zigpy.types.EUI64.convert(ieee_address)

View File

@@ -73,7 +73,6 @@ CAPABILITIES_COLOR_LOOP = 0x4
CAPABILITIES_COLOR_XY = 0x08
CAPABILITIES_COLOR_TEMP = 0x10
DEFAULT_TRANSITION = 1
DEFAULT_MIN_BRIGHTNESS = 2
UPDATE_COLORLOOP_ACTION = 0x1
@@ -119,7 +118,7 @@ class BaseLight(LogMixin, light.LightEntity):
"""Operations common to all light entities."""
_FORCE_ON = False
_DEFAULT_COLOR_FROM_OFF_TRANSITION = 0
_DEFAULT_MIN_TRANSITION_TIME = 0
def __init__(self, *args, **kwargs):
"""Initialize the light."""
@@ -140,8 +139,8 @@ class BaseLight(LogMixin, light.LightEntity):
self._level_channel = None
self._color_channel = None
self._identify_channel = None
self._default_transition = None
self._color_mode = ColorMode.UNKNOWN # Set by sub classes
self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME
self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes
@property
def extra_state_attributes(self) -> dict[str, Any]:
@@ -159,11 +158,6 @@ class BaseLight(LogMixin, light.LightEntity):
return False
return self._state
@property
def color_mode(self):
"""Return the color mode of this light."""
return self._color_mode
@property
def brightness(self):
"""Return the brightness of this light."""
@@ -221,33 +215,49 @@ class BaseLight(LogMixin, light.LightEntity):
transition = kwargs.get(light.ATTR_TRANSITION)
duration = (
transition * 10
if transition
else self._default_transition * 10
if self._default_transition
else DEFAULT_TRANSITION
)
if transition is not None
else self._zha_config_transition * 10
) or self._DEFAULT_MIN_TRANSITION_TIME # if 0 is passed in some devices still need the minimum default
brightness = kwargs.get(light.ATTR_BRIGHTNESS)
effect = kwargs.get(light.ATTR_EFFECT)
flash = kwargs.get(light.ATTR_FLASH)
temperature = kwargs.get(light.ATTR_COLOR_TEMP)
hs_color = kwargs.get(light.ATTR_HS_COLOR)
# If the light is currently off but a turn_on call with a color/temperature is sent,
# the light needs to be turned on first at a low brightness level where the light is immediately transitioned
# to the correct color. Afterwards, the transition is only from the low brightness to the new brightness.
# Otherwise, the transition is from the color the light had before being turned on to the new color.
# This can look especially bad with transitions longer than a second.
color_provided_from_off = (
not self._state
# This can look especially bad with transitions longer than a second. We do not want to do this for
# devices that need to be forced to use the on command because we would end up with 4 commands sent:
# move to level, on, color, move to level... We also will not set this if the bulb is already in the
# desired color mode with the desired color or color temperature.
new_color_provided_while_off = (
not isinstance(self, LightGroup)
and not self._FORCE_ON
and not self._state
and (
(
temperature is not None
and (
self._color_temp != temperature
or self._attr_color_mode != ColorMode.COLOR_TEMP
)
)
or (
hs_color is not None
and (
self.hs_color != hs_color
or self._attr_color_mode != ColorMode.HS
)
)
)
and brightness_supported(self._attr_supported_color_modes)
and (light.ATTR_COLOR_TEMP in kwargs or light.ATTR_HS_COLOR in kwargs)
)
final_duration = duration
if color_provided_from_off:
# Set the duration for the color changing commands to 0.
duration = 0
if (
brightness is None
and (self._off_with_transition or color_provided_from_off)
and (self._off_with_transition or new_color_provided_while_off)
and self._off_brightness is not None
):
brightness = self._off_brightness
@@ -259,11 +269,11 @@ class BaseLight(LogMixin, light.LightEntity):
t_log = {}
if color_provided_from_off:
if new_color_provided_while_off:
# If the light is currently off, we first need to turn it on at a low brightness level with no transition.
# After that, we set it to the desired color/temperature with no transition.
result = await self._level_channel.move_to_level_with_on_off(
DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_COLOR_FROM_OFF_TRANSITION
DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_MIN_TRANSITION_TIME
)
t_log["move_to_level_with_on_off"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
@@ -274,7 +284,7 @@ class BaseLight(LogMixin, light.LightEntity):
if (
(brightness is not None or transition)
and not color_provided_from_off
and not new_color_provided_while_off
and brightness_supported(self._attr_supported_color_modes)
):
result = await self._level_channel.move_to_level_with_on_off(
@@ -290,7 +300,7 @@ class BaseLight(LogMixin, light.LightEntity):
if (
brightness is None
and not color_provided_from_off
and not new_color_provided_while_off
or (self._FORCE_ON and brightness)
):
# since some lights don't always turn on with move_to_level_with_on_off,
@@ -302,34 +312,41 @@ class BaseLight(LogMixin, light.LightEntity):
return
self._state = True
if light.ATTR_COLOR_TEMP in kwargs:
temperature = kwargs[light.ATTR_COLOR_TEMP]
result = await self._color_channel.move_to_color_temp(temperature, duration)
if temperature is not None:
result = await self._color_channel.move_to_color_temp(
temperature,
self._DEFAULT_MIN_TRANSITION_TIME
if new_color_provided_while_off
else duration,
)
t_log["move_to_color_temp"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
return
self._color_mode = ColorMode.COLOR_TEMP
self._attr_color_mode = ColorMode.COLOR_TEMP
self._color_temp = temperature
self._hs_color = None
if light.ATTR_HS_COLOR in kwargs:
hs_color = kwargs[light.ATTR_HS_COLOR]
if hs_color is not None:
xy_color = color_util.color_hs_to_xy(*hs_color)
result = await self._color_channel.move_to_color(
int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration
int(xy_color[0] * 65535),
int(xy_color[1] * 65535),
self._DEFAULT_MIN_TRANSITION_TIME
if new_color_provided_while_off
else duration,
)
t_log["move_to_color"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
return
self._color_mode = ColorMode.HS
self._attr_color_mode = ColorMode.HS
self._hs_color = hs_color
self._color_temp = None
if color_provided_from_off:
if new_color_provided_while_off:
# The light is has the correct color, so we can now transition it to the correct brightness level.
result = await self._level_channel.move_to_level(level, final_duration)
result = await self._level_channel.move_to_level(level, duration)
t_log["move_to_level_if_color"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
@@ -376,12 +393,13 @@ class BaseLight(LogMixin, light.LightEntity):
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
duration = kwargs.get(light.ATTR_TRANSITION)
transition = kwargs.get(light.ATTR_TRANSITION)
supports_level = brightness_supported(self._attr_supported_color_modes)
if duration and supports_level:
# is not none looks odd here but it will override built in bulb transition times if we pass 0 in here
if transition is not None and supports_level:
result = await self._level_channel.move_to_level_with_on_off(
0, duration * 10
0, transition * 10
)
else:
result = await self._on_off_channel.off()
@@ -392,7 +410,7 @@ class BaseLight(LogMixin, light.LightEntity):
if supports_level:
# store current brightness so that the next turn_on uses it.
self._off_with_transition = bool(duration)
self._off_with_transition = transition is not None
self._off_brightness = self._brightness
self.async_write_ha_state()
@@ -451,13 +469,13 @@ class Light(BaseLight, ZhaEntity):
self._attr_supported_color_modes
)
if len(self._attr_supported_color_modes) == 1:
self._color_mode = next(iter(self._attr_supported_color_modes))
self._attr_color_mode = next(iter(self._attr_supported_color_modes))
else: # Light supports color_temp + hs, determine which mode the light is in
assert self._color_channel
if self._color_channel.color_mode == Color.ColorMode.Color_temperature:
self._color_mode = ColorMode.COLOR_TEMP
self._attr_color_mode = ColorMode.COLOR_TEMP
else:
self._color_mode = ColorMode.HS
self._attr_color_mode = ColorMode.HS
if self._identify_channel:
self._supported_features |= light.LightEntityFeature.FLASH
@@ -465,7 +483,7 @@ class Light(BaseLight, ZhaEntity):
if effect_list:
self._effect_list = effect_list
self._default_transition = async_get_zha_config_value(
self._zha_config_transition = async_get_zha_config_value(
zha_device.gateway.config_entry,
ZHA_OPTIONS,
CONF_DEFAULT_LIGHT_TRANSITION,
@@ -477,6 +495,7 @@ class Light(BaseLight, ZhaEntity):
"""Set the state."""
self._state = bool(value)
if value:
self._off_with_transition = False
self._off_brightness = None
self.async_write_ha_state()
@@ -518,7 +537,7 @@ class Light(BaseLight, ZhaEntity):
if "off_brightness" in last_state.attributes:
self._off_brightness = last_state.attributes["off_brightness"]
if "color_mode" in last_state.attributes:
self._color_mode = ColorMode(last_state.attributes["color_mode"])
self._attr_color_mode = ColorMode(last_state.attributes["color_mode"])
if "color_temp" in last_state.attributes:
self._color_temp = last_state.attributes["color_temp"]
if "hs_color" in last_state.attributes:
@@ -558,13 +577,13 @@ class Light(BaseLight, ZhaEntity):
if (color_mode := results.get("color_mode")) is not None:
if color_mode == Color.ColorMode.Color_temperature:
self._color_mode = ColorMode.COLOR_TEMP
self._attr_color_mode = ColorMode.COLOR_TEMP
color_temp = results.get("color_temperature")
if color_temp is not None and color_mode:
self._color_temp = color_temp
self._hs_color = None
else:
self._color_mode = ColorMode.HS
self._attr_color_mode = ColorMode.HS
color_x = results.get("current_x")
color_y = results.get("current_y")
if color_x is not None and color_y is not None:
@@ -610,7 +629,7 @@ class HueLight(Light):
@STRICT_MATCH(
channel_names=CHANNEL_ON_OFF,
aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
manufacturers={"Jasco", "Quotra-Vision"},
manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"},
)
class ForceOnLight(Light):
"""Representation of a light which does not respect move_to_level_with_on_off."""
@@ -626,7 +645,7 @@ class ForceOnLight(Light):
class SengledLight(Light):
"""Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition."""
_DEFAULT_COLOR_FROM_OFF_TRANSITION = 1
_DEFAULT_MIN_TRANSITION_TIME = 1
@GROUP_MATCH()
@@ -644,13 +663,13 @@ class LightGroup(BaseLight, ZhaGroupEntity):
self._color_channel = group.endpoint[Color.cluster_id]
self._identify_channel = group.endpoint[Identify.cluster_id]
self._debounced_member_refresh = None
self._default_transition = async_get_zha_config_value(
self._zha_config_transition = async_get_zha_config_value(
zha_device.gateway.config_entry,
ZHA_OPTIONS,
CONF_DEFAULT_LIGHT_TRANSITION,
0,
)
self._color_mode = None
self._attr_color_mode = None
async def async_added_to_hass(self):
"""Run when about to be added to hass."""

View File

@@ -4,15 +4,15 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [
"bellows==0.31.0",
"bellows==0.31.1",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.77",
"zigpy-deconz==0.18.0",
"zigpy==0.47.2",
"zigpy==0.47.3",
"zigpy-xbee==0.15.0",
"zigpy-zigate==0.9.0",
"zigpy-znp==0.8.0"
"zigpy-znp==0.8.1"
],
"usb": [
{

View File

@@ -94,20 +94,23 @@ def get_device_entities(
# If the value ID returns as None, we don't need to include this entity
if (value_id := get_value_id_from_unique_id(entry.unique_id)) is None:
continue
state_key = get_state_key_from_unique_id(entry.unique_id)
zwave_value = node.values[value_id]
primary_value_data = {
"command_class": zwave_value.command_class,
"command_class_name": zwave_value.command_class_name,
"endpoint": zwave_value.endpoint,
"property": zwave_value.property_,
"property_name": zwave_value.property_name,
"property_key": zwave_value.property_key,
"property_key_name": zwave_value.property_key_name,
}
if state_key is not None:
primary_value_data["state_key"] = state_key
primary_value_data = None
if (zwave_value := node.values.get(value_id)) is not None:
primary_value_data = {
"command_class": zwave_value.command_class,
"command_class_name": zwave_value.command_class_name,
"endpoint": zwave_value.endpoint,
"property": zwave_value.property_,
"property_name": zwave_value.property_name,
"property_key": zwave_value.property_key,
"property_key_name": zwave_value.property_key_name,
}
state_key = get_state_key_from_unique_id(entry.unique_id)
if state_key is not None:
primary_value_data["state_key"] = state_key
entity = {
"domain": entry.domain,
"entity_id": entry.entity_id,

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