Compare commits

..

123 Commits

Author SHA1 Message Date
Paulus Schoutsen
ddbadb1e26 Merge pull request #40310 from home-assistant/rc 2020-09-19 23:06:09 +02:00
Felipe Martins Diel
cf81a5c09a Improve tests for Broadlink config flow (#39894) 2020-09-19 20:37:20 +00:00
Paulus Schoutsen
a8b6464d7f Bumped version to 0.115.2 2020-09-19 20:13:52 +00:00
Rob Bierbooms
85ae63c656 Fix error creating duplicate ConfigEntry upon import for rfxtrx (#40296) 2020-09-19 20:13:44 +00:00
Alexei Chetroi
68cc34df6f Update ZHA dependencies (#40283) 2020-09-19 20:13:23 +00:00
Daniel Shokouhi
9955e7e5e1 Bump hangups to 0.4.11 (#40258) 2020-09-19 20:11:41 +00:00
Bram Kragten
ab8ef1c9e1 Updated frontend to 20200918.0 (#40253) 2020-09-19 20:11:40 +00:00
Robert Van Gorkom
7e6d64a24c Fix high CPU usage in vera integration. (#40249) 2020-09-19 20:11:39 +00:00
On Freund
e582caccc9 Fix Kodi discovery title (#40247) 2020-09-19 20:11:39 +00:00
Felipe Martins Diel
1eb8035122 Handle an unsupported device in the Broadlink config flow (#40242) 2020-09-19 20:11:38 +00:00
jan iversen
57b7ed6a07 Correct modbus switch to return correct coil (#40190) 2020-09-19 20:11:37 +00:00
Erik Montnemery
d35f06ac15 Get option flow defaults from yaml for non configured MQTT options (#40177) 2020-09-19 20:11:36 +00:00
Paulus Schoutsen
bf741c1b26 Merge pull request #40243 from home-assistant/rc 2020-09-18 16:51:17 +02:00
Paulus Schoutsen
2df709c7d0 Bumped version to 0.115.1 2020-09-18 14:05:11 +00:00
On Freund
e9b355bd8a Fix coolmaster.info (#40240) 2020-09-18 14:04:57 +00:00
On Freund
ef279b125d Handle systems without groups (#40238) 2020-09-18 14:04:57 +00:00
On Freund
152b380a2f Fix kodi.call_method (#40236) 2020-09-18 14:04:56 +00:00
Felipe Martins Diel
8a39bea761 Fix RM mini 3 update manager (#40215) 2020-09-18 14:04:55 +00:00
Chris Caron
d37fe1fbb6 Disable async on Apprise (#40213) 2020-09-18 14:04:54 +00:00
MeIchthys
33b56b0cf9 Fix Nextcloud sensors becoming unavailable (#40212) 2020-09-18 14:04:53 +00:00
Raman Gupta
0383030266 Fix Vizio async_unload_entry bug (#40210) 2020-09-18 14:04:52 +00:00
Julius Mittenzwei
b8fe0c6c3a Upgrade pyvlx to 0.2.17 (#40182) 2020-09-18 14:04:52 +00:00
J. Nick Koston
7cb0c98c03 Log template listeners when debug logging is on (#40180) 2020-09-18 14:04:51 +00:00
Paulus Schoutsen
58c6702080 Merge pull request #40179 from home-assistant/rc 2020-09-17 17:37:29 +02:00
Franck Nijhof
f77b3d4714 Bumped version to 0.115.0 2020-09-17 17:10:26 +02:00
J. Nick Koston
f5aee6b886 Add missing conext preservation to bayesian and universal (#40178)
We already do this for template sensors, but it was
missing for bayesian and universal
2020-09-17 17:07:52 +02:00
Bram Kragten
6f26722f69 Fix editing tags only get isoformat from datetime (#40174)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-09-17 17:07:49 +02:00
Bram Kragten
13cfd1bae1 Updated frontend to 20200917.1 (#40170) 2020-09-17 17:07:45 +02:00
Joakim Sørensen
5271a3eb1e Update pyhaversion to 3.4.2 (#40161) 2020-09-17 17:07:42 +02:00
cagnulein
78022bf145 Fix luci device_tracker not reliably reporting home/away state (#40160) 2020-09-17 17:07:39 +02:00
Chris Talkington
99a57f5a4e Check mpd time type before splitting it (#40139) 2020-09-17 17:07:33 +02:00
Paulus Schoutsen
a9e220c96b Bumped version to 0.115.0b12 2020-09-16 20:50:22 +00:00
Bram Kragten
f7d7765d5e Update frontend to 20200916.0 (#40153) 2020-09-16 20:42:50 +00:00
Franck Nijhof
9ffcf35b23 Fix local media browser source conflicting with local www folder (#40151) 2020-09-16 20:42:49 +00:00
Franck Nijhof
d3a59652bb Fix missing f from f-strings in cast integration (#40144) 2020-09-16 20:42:48 +00:00
Paulus Schoutsen
c62a6cd779 Fix scene validator (#40140) 2020-09-16 20:42:47 +00:00
Paulus Schoutsen
f1169120ae Bumped version to 0.115.0b11 2020-09-16 13:37:51 +00:00
Paulus Schoutsen
b28dbe20b6 Fix ESPHome scan tag device ID (#40132) 2020-09-16 13:37:34 +00:00
Paulus Schoutsen
8dde59be02 Guard for when Yandex Transport data fetching fails (#40131) 2020-09-16 13:37:33 +00:00
Jesse Hills
abca177894 Use device name stored in device_info for tag scan in ESPHome (#40130) 2020-09-16 13:37:32 +00:00
Jesse Hills
d3bb2e5e16 Allow ESPHome to trigger the HA tag scanned event (#40128) 2020-09-16 13:37:31 +00:00
Maciej Bieniek
7f8a89838b Bump aioshelly library to version 0.3.2 (#40118) 2020-09-16 13:36:01 +00:00
springstan
39c4b338f1 Increase TIMEOUT_ACK to 10s (#40117) 2020-09-16 13:33:48 +00:00
Maciej Bieniek
4518335a56 Remove the unnecessary prefix from the sensor names in the Shelly integration (#40097) 2020-09-16 13:33:47 +00:00
Shay Levy
b856b0e15d Guard both Shelly 2 & Shelly 2.5 in roller mode (#40086)
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
2020-09-16 13:33:46 +00:00
Franck Nijhof
5d518b5365 Add media dirs core configuration (#40071)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-09-16 13:33:46 +00:00
Diogo Gomes
ce86112612 Address error in SQL query (#39939) 2020-09-16 13:33:45 +00:00
Bram Kragten
e095120023 Bumped version to 0.115.0b10 2020-09-15 16:21:32 +02:00
Bram Kragten
3ef3d848f7 Update frontend to 20200915.0 (#40101) 2020-09-15 16:19:42 +02:00
J. Nick Koston
610a327b52 Convert color temperature to hue and saturation for HomeKit (#40089)
The HomeKit spec does not permit the color temp characteristic
being exposed when color (hue, sat) is present.  Since
Home Assistant will still send color temp values, we need to
convert them to hue, sat values for HomeKit
2020-09-15 16:19:41 +02:00
cgtobi
81436fb688 Check Sonos for local library before browsing (#40085) 2020-09-15 16:19:40 +02:00
Alexei Chetroi
24fe9cdd5a Update ZHA dependency (#40083) 2020-09-15 16:19:39 +02:00
Erik Montnemery
e5c499c22e Increase TIMEOUT_ACK to 2s (#40080) 2020-09-15 16:19:39 +02:00
cgtobi
99a8604601 Fix netatmo media browser of outdoor events (#40079)
* Fix outdoor events

* Fix test data

* Increase coverage
2020-09-15 16:19:38 +02:00
Paulus Schoutsen
3ef821d62f Fix tag last scanned serialization (#40067) 2020-09-15 16:19:37 +02:00
Pascal Vizeli
a38e047e83 Update docker base image to 8.4.0 (#40066) 2020-09-15 16:19:36 +02:00
Jc2k
e0fcf9b648 Bump aiohomekit version (regression fix) (#40064) 2020-09-15 16:18:08 +02:00
Evgeny
0e823b566b Fix default forecast mode OpenWeatherMap (#40062) 2020-09-15 16:00:28 +02:00
Markus Bong
a9d24c2cd5 Correct devolo climate devices (#40061) 2020-09-15 16:00:27 +02:00
b3nj1
7a7cad39eb Fix ecobee weather forcast off by 1 bug (#40048) 2020-09-15 16:00:26 +02:00
J. Nick Koston
1a76a953c7 Update gogogate2-api to 2.0.2 (#40010)
* Update gogogate2-api to 2.0.2

Resolves a timeout issue: https://github.com/vangorra/python_gogogate2_api/pull/11

* mock voltage
2020-09-15 16:00:25 +02:00
r4nd0mbr1ck
db27079fa8 Speedtestdotnet - use server name to generate server list (#39775) 2020-09-15 16:00:24 +02:00
Paulus Schoutsen
ef1649383c Bumped version to 0.115.0b9 2020-09-13 20:22:47 +00:00
Franck Nijhof
afde5a7ece Fix entity extraction from Template conditions (#40034) 2020-09-13 20:22:38 +00:00
J. Nick Koston
30b8565548 Ensure homekit_controller traps exceptions from find_ip_by_device_id (#40030) 2020-09-13 20:22:38 +00:00
springstan
a971b92899 Fix slack notifications requiring an icon (#40027) 2020-09-13 20:22:37 +00:00
J. Nick Koston
4ee7cdc8a0 Do not log an error when a host is unreachable while pinging (#40024) 2020-09-13 20:22:36 +00:00
Robert Svensson
4c2788a13c Improve handling of mireds being far out of spec (#40018) 2020-09-13 20:22:36 +00:00
Bouwe Westerdijk
8b4e193614 Ensure Plugwise unique_id is correctly set (#40014)
* Ensure unique_id is correctly set

* Removed unnec. line

Co-authored-by: Tom Scholten <git@scholten.nu>
2020-09-13 20:22:35 +00:00
Tom
f0ce65af7d Add tests for Plugwise integration (#36371) 2020-09-13 20:22:34 +00:00
springstan
b81c61dd99 Fix requiring username or password for nzbget yaml config (#40003) 2020-09-13 20:18:57 +00:00
J. Nick Koston
30ef7a5e88 Suppress homekit bridge discovery by homekit controller (#39990) 2020-09-13 20:18:56 +00:00
Pascal Vizeli
5a6492b76d Update azure-pipelines-wheels.yml 2020-09-13 15:38:31 +02:00
Bram Kragten
b19fe17e76 Bumped version to 0.115.0b8 2020-09-13 11:41:46 +02:00
Joakim Sørensen
47326b2295 Bump pyhaversion to 3.4.0 (#40016) 2020-09-13 11:40:19 +02:00
Quentame
951c373110 Fix Freebox call sensor when no call in history (#40001) 2020-09-13 11:40:19 +02:00
Jeff Irion
b9b76b3519 Bump androidtv to 0.0.50 (#39998) 2020-09-13 11:40:17 +02:00
Joakim Sørensen
da6885af6c Bump frontend to 20200912.0 (#39997) 2020-09-13 11:40:16 +02:00
jjlawren
bc2173747c Fix children_media_class for special folders (#39974) 2020-09-13 11:40:15 +02:00
uvjustin
d0e6b3e268 Remove skip_sidx container option in stream (#39970)
* Remove skip_sidx container option

* Add comment
2020-09-13 11:40:14 +02:00
Paulus Schoutsen
172a02a605 Bumped version to 0.115.0b7 2020-09-12 12:28:57 +00:00
Bram Kragten
b6f868f629 Add children media class to children spotify media browser (#39953) 2020-09-12 12:28:47 +00:00
uvjustin
5697f4b4e7 Set output timescale to input timescale (#39946) 2020-09-12 12:28:47 +00:00
J. Nick Koston
30f9e1b479 Change template loop detection strategy to allow self-referencing updates when there are multiple templates (#39943) 2020-09-12 12:28:46 +00:00
Quentame
fcbcebea9b Fix missing position attribute for MeteoFranceAlertSensor (#39938) 2020-09-12 12:28:45 +00:00
J. Nick Koston
f81606cbf5 Return the listeners with the template result for the websocket api (#39925) 2020-09-12 12:28:44 +00:00
Greg Dowling
3240be0bb6 Bump pyloopenergy library to 0.2.1 (#39919) 2020-09-12 12:28:44 +00:00
On Freund
18be6cbadc Handle Kodi shutdown (#39856)
* Handle Kodi shutdown

* Core review comments

* Make async_on_quit a coroutine
2020-09-12 12:28:43 +00:00
Paulus Schoutsen
a002e9b12f Bumped version to 0.115.0b6 2020-09-11 12:18:53 +00:00
Franck Nijhof
db64a9ebfa Accept known hosts for get_url for OAuth (#39936) 2020-09-11 12:17:24 +00:00
J. Nick Koston
3fbde22cc4 Update zeroconf to 0.28.5 (#39923) 2020-09-11 12:17:24 +00:00
J. Nick Koston
758e60a58d Prevent missing integration from failing HomeKit startup (#39918) 2020-09-11 12:17:23 +00:00
Paulus Schoutsen
5201410e39 Bump aioshelly to 0.3.1 (#39917) 2020-09-11 12:16:43 +00:00
Thomas Lovén
b1b7944012 Set variable values in scripts (#39915)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-09-11 12:16:01 +00:00
Paulus Schoutsen
8ef04268be Extract variable rendering (#39934) 2020-09-11 12:15:28 +00:00
Bas Nijholt
b107e87d38 Don't trigger on attribute when the attribute doesn't change (#39910)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-09-11 12:12:58 +00:00
uvjustin
b0b9579778 Disable audio for HLS or mpegts input (#39906) 2020-09-11 12:12:57 +00:00
Martin Hjelmare
7eade4029a Add children media class (#39902)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-09-11 12:12:57 +00:00
Marvin Wichmann
3d4913348a Warn users if KNX has no devices configured (#39899)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2020-09-11 12:12:56 +00:00
J. Nick Koston
1720b71d62 Limit zeroconf discovery to name/macaddress when provided (#39877)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-09-11 12:12:27 +00:00
Paulus Schoutsen
589086f0d0 Bumped version to 0.115.0b5 2020-09-10 18:53:37 +00:00
Erik Montnemery
6f8060dea7 Fix discovery update of MQTT state templates (#39901) 2020-09-10 18:53:25 +00:00
J. Nick Koston
b8ef87d84c Fix ping log level to be debug instead of warning (#39900) 2020-09-10 18:53:25 +00:00
J. Nick Koston
7370b0ffc6 Detect self-referencing loops in template entities and log a warning (#39897) 2020-09-10 18:53:24 +00:00
Paulus Schoutsen
209cf44e8e Add default variables to script helper (#39895) 2020-09-10 18:53:23 +00:00
Pascal Vizeli
b7dacabbe4 Fix issue with grpcio build on 32bit arch (#39893) 2020-09-10 18:53:22 +00:00
Bram Kragten
5098c35814 Fix spotify media browser category (#39888) 2020-09-10 18:53:21 +00:00
Paulus Schoutsen
896df60f32 Shelly switch to guard for shelly 2 in roller mode (#39886) 2020-09-10 18:53:21 +00:00
Pascal Vizeli
b26ab2849b Bump hass-nabucasa 0.37.0 (#39885) 2020-09-10 18:53:20 +00:00
Paulus Schoutsen
36f52a26f6 Fix event trigger (#39884)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2020-09-10 18:53:19 +00:00
bsmappee
f0295d562d Bump pysmappee to 0.2.13 (#39883) 2020-09-10 18:53:18 +00:00
Joakim Sørensen
081bd22e59 Updated frontend to 20200909.0 (#39869)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-10 18:53:18 +00:00
Franck Nijhof
668c73010a Disable Met.no hourly weather by default (#39867)
Co-authored-by: Daniel Hjelseth Høyer <mail@dahoiv.net>
2020-09-10 18:53:17 +00:00
Paulus Schoutsen
fe371f0438 Install stdlib-list in script/bootstrap (#39866) 2020-09-10 18:53:16 +00:00
Joakim Sørensen
be28dc0bca Add exception for NoURLAvailableError in OAuth2FlowHandler (#39845)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-09-10 18:53:15 +00:00
Chris Talkington
4578baca3e Improve Roku media browser structure (#39754)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2020-09-10 18:53:15 +00:00
Paulus Schoutsen
6d7dfc0804 Bumped version to 0.115.0b4 2020-09-09 20:35:18 +00:00
Martin Hjelmare
c5cf95c14b Remove media class apps and channels (#39864) 2020-09-09 20:34:15 +00:00
Alexei Chetroi
f79ce7bd04 Update ZHA dependency (#39862) 2020-09-09 20:34:14 +00:00
Jason Hunter
578c1b283a Sort Local Media Source and fix media class (#39858) 2020-09-09 20:34:13 +00:00
Martin
5ae0844f35 Updated warning_device_warn (#39851)
duty_cycle: spec says in inrements of 10
duration: its a 16 bit field
2020-09-09 20:34:13 +00:00
J. Nick Koston
8e3e2d436e Use a unique id for each icmplib ping to avoid mixing unrelated responses (#39830) 2020-09-09 20:34:12 +00:00
Colin Frei
4af6804c50 Use correct URL for Fitbit callbacks (#39823) 2020-09-09 20:34:11 +00:00
217 changed files with 4409 additions and 762 deletions

View File

@@ -656,11 +656,6 @@ omit =
homeassistant/components/plaato/*
homeassistant/components/plex/media_player.py
homeassistant/components/plex/sensor.py
homeassistant/components/plugwise/__init__.py
homeassistant/components/plugwise/binary_sensor.py
homeassistant/components/plugwise/climate.py
homeassistant/components/plugwise/sensor.py
homeassistant/components/plugwise/switch.py
homeassistant/components/plum_lightpad/light.py
homeassistant/components/pocketcasts/sensor.py
homeassistant/components/point/*

View File

@@ -236,6 +236,7 @@ homeassistant/components/linux_battery/* @fabaff
homeassistant/components/local_ip/* @issacg
homeassistant/components/logger/* @home-assistant/core
homeassistant/components/logi_circle/* @evanjd
homeassistant/components/loopenergy/* @pavoni
homeassistant/components/lovelace/* @home-assistant/frontend
homeassistant/components/luci/* @fbradyirl @mzdrale
homeassistant/components/luftdaten/* @fabaff
@@ -465,7 +466,7 @@ homeassistant/components/velbus/* @Cereal2nd @brefra
homeassistant/components/velux/* @Julius2342
homeassistant/components/vera/* @vangorra
homeassistant/components/versasense/* @flamm3blemuff1n
homeassistant/components/version/* @fabaff
homeassistant/components/version/* @fabaff @ludeeus
homeassistant/components/vesync/* @markperdue @webdjoe @thegardenmonkey
homeassistant/components/vicare/* @oischinger
homeassistant/components/vilfo/* @ManneW

View File

@@ -47,8 +47,9 @@ jobs:
- template: templates/azp-job-wheels.yaml@azure
parameters:
builderVersion: '$(versionWheels)'
builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev'
builderApk: 'build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev'
builderPip: 'Cython;numpy;scikit-build'
builderEnvFile: true
skipBinary: 'aiohttp'
wheelsRequirement: 'requirements_wheels.txt'
wheelsRequirementDiff: 'requirements_diff.txt'
@@ -90,4 +91,10 @@ jobs:
sed -i "s|# bme680|bme680|g" ${requirement_file}
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
done
# Write env for build settings
(
echo "GRPC_BUILD_WITH_BORING_SSL_ASM="
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1"
) > .env_file
displayName: 'Prepare requirements files for Home Assistant wheels'

View File

@@ -1,11 +1,11 @@
{
"image": "homeassistant/{arch}-homeassistant",
"build_from": {
"aarch64": "homeassistant/aarch64-homeassistant-base:8.3.0",
"armhf": "homeassistant/armhf-homeassistant-base:8.3.0",
"armv7": "homeassistant/armv7-homeassistant-base:8.3.0",
"amd64": "homeassistant/amd64-homeassistant-base:8.3.0",
"i386": "homeassistant/i386-homeassistant-base:8.3.0"
"aarch64": "homeassistant/aarch64-homeassistant-base:8.4.0",
"armhf": "homeassistant/armhf-homeassistant-base:8.4.0",
"armv7": "homeassistant/armv7-homeassistant-base:8.4.0",
"amd64": "homeassistant/amd64-homeassistant-base:8.4.0",
"i386": "homeassistant/i386-homeassistant-base:8.4.0"
},
"labels": {
"io.hass.type": "core"

View File

@@ -10,7 +10,8 @@
"abort": {
"already_setup": "You can only configure one Almond account.",
"cannot_connect": "Unable to connect to the Almond server.",
"missing_configuration": "Please check the documentation on how to set up Almond."
"missing_configuration": "Please check the documentation on how to set up Almond.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
}
}
}

View File

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

View File

@@ -380,7 +380,7 @@ def adb_decorator(override_available=False):
# An unforeseen exception occurred. Close the ADB connection so that
# it doesn't happen over and over again, then raise the exception.
await self.aftv.adb_close()
self._available = False # pylint: disable=protected-access
self._available = False
raise
return _adb_exception_catcher

View File

@@ -29,8 +29,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def get_service(hass, config, discovery_info=None):
"""Get the Apprise notification service."""
# Create our object
a_obj = apprise.Apprise()
# Create our Apprise Asset Object
asset = apprise.AppriseAsset(async_mode=False)
# Create our Apprise Instance (reference our asset)
a_obj = apprise.Apprise(asset=asset)
if config.get(CONF_FILE):
# Sourced from a Configuration File

View File

@@ -13,6 +13,7 @@ from homeassistant.const import (
CONF_ID,
CONF_MODE,
CONF_PLATFORM,
CONF_VARIABLES,
CONF_ZONE,
EVENT_HOMEASSISTANT_STARTED,
SERVICE_RELOAD,
@@ -29,7 +30,7 @@ from homeassistant.core import (
split_entity_id,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import condition, extract_domain_configs
from homeassistant.helpers import condition, extract_domain_configs, template
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
@@ -44,6 +45,7 @@ from homeassistant.helpers.script import (
Script,
make_script_schema,
)
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.trigger import async_initialize_triggers
from homeassistant.helpers.typing import TemplateVarsType
@@ -104,6 +106,7 @@ PLATFORM_SCHEMA = vol.All(
vol.Optional(CONF_HIDE_ENTITY): cv.boolean,
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
},
SCRIPT_MODE_SINGLE,
@@ -239,6 +242,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
cond_func,
action_script,
initial_state,
variables,
):
"""Initialize an automation entity."""
self._id = automation_id
@@ -253,6 +257,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
self._referenced_entities: Optional[Set[str]] = None
self._referenced_devices: Optional[Set[str]] = None
self._logger = _LOGGER
self._variables: ScriptVariables = variables
@property
def name(self):
@@ -378,11 +383,20 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
else:
await self.async_disable()
async def async_trigger(self, variables, context=None, skip_condition=False):
async def async_trigger(self, run_variables, context=None, skip_condition=False):
"""Trigger automation.
This method is a coroutine.
"""
if self._variables:
try:
variables = self._variables.async_render(self.hass, run_variables)
except template.TemplateError as err:
self._logger.error("Error rendering variables: %s", err)
return
else:
variables = run_variables
if (
not skip_condition
and self._cond_func is not None
@@ -518,6 +532,9 @@ async def _async_process_config(hass, config, component):
max_runs=config_block[CONF_MAX],
max_exceeded=config_block[CONF_MAX_EXCEEDED],
logger=_LOGGER,
# We don't pass variables here
# Automation will already render them to use them in the condition
# and so will pass them on to the script.
)
if CONF_CONDITION in config_block:
@@ -535,6 +552,7 @@ async def _async_process_config(hass, config, component):
cond_func,
action_script,
initial_state,
config_block.get(CONF_VARIABLES),
)
entities.append(entity)

View File

@@ -4,7 +4,11 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/axis",
"requirements": ["axis==35"],
"zeroconf": ["_axis-video._tcp.local."],
"zeroconf": [
{"type":"_axis-video._tcp.local.","macaddress":"00408C*"},
{"type":"_axis-video._tcp.local.","macaddress":"ACCC8E*"},
{"type":"_axis-video._tcp.local.","macaddress":"B8A44F*"}
],
"after_dependencies": ["mqtt"],
"codeowners": ["@Kane610"]
}

View File

@@ -182,6 +182,7 @@ class BayesianBinarySensor(BinarySensorEntity):
entity = event.data.get("entity_id")
self.current_observations.update(self._record_entity_observations(entity))
self.async_set_context(event.context)
self._recalculate_and_write_state()
self.async_on_remove(
@@ -220,6 +221,8 @@ class BayesianBinarySensor(BinarySensorEntity):
obs_entry = None
self.current_observations[obs["id"]] = obs_entry
if event:
self.async_set_context(event.context)
self._recalculate_and_write_state()
for template in self.observations_by_template:

View File

@@ -11,7 +11,7 @@ from broadlink.exceptions import (
)
import voluptuous as vol
from homeassistant import config_entries
from homeassistant import config_entries, data_entry_flow
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE
from homeassistant.helpers import config_validation as cv
@@ -20,6 +20,7 @@ from .const import ( # pylint: disable=unused-import
DEFAULT_PORT,
DEFAULT_TIMEOUT,
DOMAIN,
DOMAINS_AND_TYPES,
)
from .helpers import format_mac
@@ -36,6 +37,19 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_set_device(self, device, raise_on_progress=True):
"""Define a device for the config flow."""
supported_types = {
device_type
for _, device_types in DOMAINS_AND_TYPES
for device_type in device_types
}
if device.type not in supported_types:
LOGGER.error(
"Unsupported device: %s. If it worked before, please open "
"an issue at https://github.com/home-assistant/core/issues",
hex(device.devtype),
)
raise data_entry_flow.AbortFlow("not_supported")
await self.async_set_unique_id(
device.mac.hex(), raise_on_progress=raise_on_progress
)

View File

@@ -35,6 +35,7 @@
"already_in_progress": "There is already a configuration flow in progress for this device",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_host": "Invalid hostname or IP address",
"not_supported": "Device not supported",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {

View File

@@ -5,6 +5,7 @@
"already_in_progress": "There is already a configuration flow in progress for this device",
"cannot_connect": "Failed to connect",
"invalid_host": "Invalid hostname or IP address",
"not_supported": "Device not supported",
"unknown": "Unexpected error"
},
"error": {

View File

@@ -1,12 +1,15 @@
"""Support for fetching data from Broadlink devices."""
from abc import ABC, abstractmethod
from datetime import timedelta
from functools import partial
import logging
import broadlink as blk
from broadlink.exceptions import (
AuthorizationError,
BroadlinkException,
CommandNotSupportedError,
DeviceOfflineError,
StorageError,
)
@@ -18,6 +21,9 @@ _LOGGER = logging.getLogger(__name__)
def get_update_manager(device):
"""Return an update manager for a given Broadlink device."""
if device.api.model.startswith("RM mini"):
return BroadlinkRMMini3UpdateManager(device)
update_managers = {
"A1": BroadlinkA1UpdateManager,
"MP1": BroadlinkMP1UpdateManager,
@@ -95,6 +101,22 @@ class BroadlinkMP1UpdateManager(BroadlinkUpdateManager):
return await self.device.async_request(self.device.api.check_power)
class BroadlinkRMMini3UpdateManager(BroadlinkUpdateManager):
"""Manages updates for Broadlink RM mini 3 devices."""
async def async_fetch_data(self):
"""Fetch data from the device."""
hello = partial(
blk.discover,
discover_ip_address=self.device.api.host[0],
timeout=self.device.api.timeout,
)
devices = await self.device.hass.async_add_executor_job(hello)
if not devices:
raise DeviceOfflineError("The device is offline")
return {}
class BroadlinkRMUpdateManager(BroadlinkUpdateManager):
"""Manages updates for Broadlink RM2 and RM4 devices."""

View File

@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/brother",
"codeowners": ["@bieniu"],
"requirements": ["brother==0.1.17"],
"zeroconf": ["_printer._tcp.local."],
"zeroconf": [{"type": "_printer._tcp.local.", "name":"brother*"}],
"config_flow": true,
"quality_scale": "platinum"
}

View File

@@ -375,9 +375,9 @@ class CastDevice(MediaPlayerEntity):
if tts_base_url and media_status.content_id.startswith(tts_base_url):
url_description = f" from tts.base_url ({tts_base_url})"
if external_url and media_status.content_id.startswith(external_url):
url_description = " from external_url ({external_url})"
url_description = f" from external_url ({external_url})"
if internal_url and media_status.content_id.startswith(internal_url):
url_description = " from internal_url ({internal_url})"
url_description = f" from internal_url ({internal_url})"
_LOGGER.error(
"Failed to cast media %s%s. Please make sure the URL is: "

View File

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

View File

@@ -3,6 +3,6 @@
"name": "CoolMasterNet",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/coolmaster",
"requirements": ["pycoolmasternet-async==0.1.1"],
"requirements": ["pycoolmasternet-async==0.1.2"],
"codeowners": ["@OnFreund"]
}

View File

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

View File

@@ -27,7 +27,7 @@ async def async_setup_entry(
for device in hass.data[DOMAIN]["homecontrol"].multi_level_switch_devices:
for multi_level_switch in device.multi_level_switch_property:
if device.deviceModelUID in [
if device.device_model_uid in [
"devolo.model.Thermostat:Valve",
"devolo.model.Room:Thermostat",
]:

View File

@@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/doorbird",
"requirements": ["doorbirdpy==2.1.0"],
"dependencies": ["http"],
"zeroconf": ["_axis-video._tcp.local."],
"zeroconf": [{"type":"_axis-video._tcp.local.","macaddress":"1CCAE3*"}],
"codeowners": ["@oblogic7", "@bdraco"],
"config_flow": true
}

View File

@@ -1,5 +1,5 @@
"""Support for displaying weather info from Ecobee API."""
from datetime import datetime
from datetime import timedelta
from pyecobee.const import ECOBEE_STATE_UNKNOWN
@@ -13,6 +13,7 @@ from homeassistant.components.weather import (
WeatherEntity,
)
from homeassistant.const import TEMP_FAHRENHEIT
from homeassistant.util import dt as dt_util
from .const import (
_LOGGER,
@@ -165,10 +166,13 @@ class EcobeeWeather(WeatherEntity):
return None
forecasts = []
for day in range(1, 5):
date = dt_util.utcnow()
for day in range(0, 5):
forecast = _process_forecast(self.weather["forecasts"][day])
if forecast is None:
continue
forecast[ATTR_FORECAST_TIME] = date.isoformat()
date += timedelta(days=1)
forecasts.append(forecast)
if forecasts:
@@ -186,9 +190,6 @@ def _process_forecast(json):
"""Process a single ecobee API forecast to return expected values."""
forecast = {}
try:
forecast[ATTR_FORECAST_TIME] = datetime.strptime(
json["dateTime"], "%Y-%m-%d %H:%M:%S"
).isoformat()
forecast[ATTR_FORECAST_CONDITION] = ECOBEE_WEATHER_SYMBOL_TO_HASS[
json["weatherSymbol"]
]

View File

@@ -66,6 +66,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
password = entry.data[CONF_PASSWORD]
device_id = None
zeroconf_instance = await zeroconf.async_get_instance(hass)
@@ -129,6 +130,15 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
"Can only generate events under esphome domain! (%s)", host
)
return
# Call native tag scan
if service_name == "tag_scanned":
tag_id = service_data["tag_id"]
hass.async_create_task(
hass.components.tag.async_scan_tag(tag_id, device_id)
)
return
hass.bus.async_fire(service.service, service_data)
else:
hass.async_create_task(
@@ -166,10 +176,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
async def on_login() -> None:
"""Subscribe to states and list entities on successful API login."""
nonlocal device_id
try:
entry_data.device_info = await cli.device_info()
entry_data.available = True
await _async_setup_device_registry(hass, entry, entry_data.device_info)
device_id = await _async_setup_device_registry(
hass, entry, entry_data.device_info
)
entry_data.async_update_device_state(hass)
entity_infos, services = await cli.list_entities_services()
@@ -265,7 +278,7 @@ async def _async_setup_device_registry(
if device_info.compilation_time:
sw_version += f" ({device_info.compilation_time})"
device_registry = await dr.async_get_registry(hass)
device_registry.async_get_or_create(
entry = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)},
name=device_info.name,
@@ -273,6 +286,7 @@ async def _async_setup_device_registry(
model=device_info.model,
sw_version=sw_version,
)
return entry.id
async def _register_service(

View File

@@ -6,7 +6,5 @@
"requirements": ["aioesphomeapi==2.6.3"],
"zeroconf": ["_esphomelib._tcp.local."],
"codeowners": ["@OttoWinter"],
"after_dependencies": [
"zeroconf"
]
"after_dependencies": ["zeroconf", "tag"]
}

View File

@@ -185,7 +185,9 @@ def request_app_setup(hass, config, add_entities, config_path, discovery_info=No
else:
setup_platform(hass, config, add_entities, discovery_info)
start_url = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}"
start_url = (
f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}"
)
description = f"""Please create a Fitbit developer app at
https://dev.fitbit.com/apps/new.
@@ -220,7 +222,7 @@ def request_oauth_completion(hass):
def fitbit_configuration_callback(callback_data):
"""Handle configuration updates."""
start_url = f"{get_url(hass)}{FITBIT_AUTH_START}"
start_url = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_START}"
description = f"Please authorize Fitbit by visiting {start_url}"
@@ -312,7 +314,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
config_file.get(CONF_CLIENT_ID), config_file.get(CONF_CLIENT_SECRET)
)
redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}"
redirect_uri = (
f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}"
)
fitbit_auth_start_url, _ = oauth.authorize_token_url(
redirect_uri=redirect_uri,
@@ -357,7 +361,7 @@ class FitbitAuthCallbackView(HomeAssistantView):
result = None
if data.get("code") is not None:
redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}"
redirect_uri = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}"
try:
result = self.oauth.fetch_access_token(data.get("code"), redirect_uri)

View File

@@ -146,11 +146,12 @@ class FreeboxCallSensor(FreeboxSensor):
def async_update_state(self) -> None:
"""Update the Freebox call sensor."""
self._call_list_for_type = []
for call in self._router.call_list:
if not call["new"]:
continue
if call["type"] == self._sensor_type:
self._call_list_for_type.append(call)
if self._router.call_list:
for call in self._router.call_list:
if not call["new"]:
continue
if call["type"] == self._sensor_type:
self._call_list_for_type.append(call)
self._state = len(self._call_list_for_type)

View File

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

View File

@@ -3,7 +3,7 @@
"name": "Gogogate2 and iSmartGate",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/gogogate2",
"requirements": ["gogogate2-api==2.0.1"],
"requirements": ["gogogate2-api==2.0.2"],
"codeowners": ["@vangorra"],
"homekit": {
"models": [

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/hangouts",
"requirements": [
"hangups==0.4.10"
"hangups==0.4.11"
],
"codeowners": []
}

View File

@@ -6,7 +6,8 @@
}
},
"abort": {
"missing_configuration": "The Home Connect component is not configured. Please follow the documentation."
"missing_configuration": "The Home Connect component is not configured. Please follow the documentation.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
},
"create_entry": {
"default": "Successfully authenticated with Home Connect."

View File

@@ -35,15 +35,15 @@ def _convert_states(states):
"""Convert state definitions to State objects."""
result = {}
for entity_id in states:
for entity_id, info in states.items():
entity_id = cv.entity_id(entity_id)
if isinstance(states[entity_id], dict):
entity_attrs = states[entity_id].copy()
if isinstance(info, dict):
entity_attrs = info.copy()
state = entity_attrs.pop(ATTR_STATE, None)
attributes = entity_attrs
else:
state = states[entity_id]
state = info
attributes = {}
# YAML translates 'on' to a boolean

View File

@@ -28,11 +28,15 @@ async def async_attach_trigger(
):
"""Listen for events based on configuration."""
event_type = config.get(CONF_EVENT_TYPE)
event_data_schema = (
vol.Schema(config.get(CONF_EVENT_DATA), extra=vol.ALLOW_EXTRA)
if config.get(CONF_EVENT_DATA)
else None
)
event_data_schema = None
if config.get(CONF_EVENT_DATA):
event_data_schema = vol.Schema(
{
vol.Required(key): value
for key, value in config.get(CONF_EVENT_DATA).items()
},
extra=vol.ALLOW_EXTRA,
)
@callback
def handle_event(event):

View File

@@ -80,6 +80,13 @@ async def async_attach_trigger(
else:
new_value = to_s.attributes.get(attribute)
# When we listen for state changes with `match_all`, we
# will trigger even if just an attribute changes. When
# we listen to just an attribute, we should ignore all
# other attribute changes.
if attribute is not None and old_value == new_value:
return
if (
not match_from_state(old_value)
or not match_to_state(new_value)

View File

@@ -38,7 +38,7 @@ from homeassistant.helpers import device_registry, entity_registry
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.loader import async_get_integration
from homeassistant.loader import IntegrationNotFound, async_get_integration
from homeassistant.util import get_local_ip
from .accessories import get_accessory
@@ -712,8 +712,13 @@ class HomeKit:
if dev_reg_ent.sw_version:
ent_cfg[ATTR_SOFTWARE_VERSION] = dev_reg_ent.sw_version
if ATTR_MANUFACTURER not in ent_cfg:
integration = await async_get_integration(self.hass, ent_reg_ent.platform)
ent_cfg[ATTR_INTERGRATION] = integration.name
try:
integration = await async_get_integration(
self.hass, ent_reg_ent.platform
)
ent_cfg[ATTR_INTERGRATION] = integration.name
except IntegrationNotFound:
ent_cfg[ATTR_INTERGRATION] = ent_reg_ent.platform
class HomeKitPairingQRView(HomeAssistantView):

View File

@@ -24,6 +24,10 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import callback
from homeassistant.util.color import (
color_temperature_mired_to_kelvin,
color_temperature_to_hs,
)
from .accessories import TYPES, HomeAccessory
from .const import (
@@ -64,8 +68,6 @@ class Light(HomeAccessory):
if self._features & SUPPORT_COLOR:
self.chars.append(CHAR_HUE)
self.chars.append(CHAR_SATURATION)
self._hue = None
self._saturation = None
elif self._features & SUPPORT_COLOR_TEMP:
# ColorTemperature and Hue characteristic should not be
# exposed both. Both states are tracked separately in HomeKit,
@@ -179,7 +181,16 @@ class Light(HomeAccessory):
# Handle Color
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
hue, saturation = new_state.attributes.get(ATTR_HS_COLOR, (None, None))
if ATTR_HS_COLOR in new_state.attributes:
hue, saturation = new_state.attributes[ATTR_HS_COLOR]
elif ATTR_COLOR_TEMP in new_state.attributes:
hue, saturation = color_temperature_to_hs(
color_temperature_mired_to_kelvin(
new_state.attributes[ATTR_COLOR_TEMP]
)
)
else:
hue, saturation = None, None
if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)):
hue = round(hue, 0)
saturation = round(saturation, 0)

View File

@@ -8,12 +8,19 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.core import callback
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
async_get_registry as async_get_device_registry,
)
from .connection import get_accessory_name, get_bridge_information
from .const import DOMAIN, KNOWN_DEVICES
HOMEKIT_IGNORE = ["Home Assistant Bridge"]
HOMEKIT_DIR = ".homekit"
HOMEKIT_BRIDGE_DOMAIN = "homekit"
HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge"
HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge"
PAIRING_FILE = "pairing.json"
PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$")
@@ -141,6 +148,17 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
return self.async_abort(reason="no_devices")
async def _hkid_is_homekit_bridge(self, hkid):
"""Determine if the device is a homekit bridge."""
dev_reg = await async_get_device_registry(self.hass)
device = dev_reg.async_get_device(
identifiers=set(), connections={(CONNECTION_NETWORK_MAC, hkid)}
)
if device is None:
return False
return device.model == HOMEKIT_BRIDGE_MODEL
async def async_step_zeroconf(self, discovery_info):
"""Handle a discovered HomeKit accessory.
@@ -153,6 +171,12 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
key.lower(): value for (key, value) in discovery_info["properties"].items()
}
if "id" not in properties:
_LOGGER.warning(
"HomeKit device %s: id not exposed, in violation of spec", properties
)
return self.async_abort(reason="invalid_properties")
# The hkid is a unique random number that looks like a pairing code.
# It changes if a device is factory reset.
hkid = properties["id"]
@@ -208,7 +232,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
# Devices in HOMEKIT_IGNORE have native local integrations - users
# should be encouraged to use native integration and not confused
# by alternative HK API.
if model in HOMEKIT_IGNORE:
if await self._hkid_is_homekit_bridge(hkid):
return self.async_abort(reason="ignored_model")
self.model = model
@@ -280,9 +304,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
# Its possible that the first try may have been busy so
# we always check to see if self.finish_paring has been
# set.
discovery = await self.controller.find_ip_by_device_id(self.hkid)
try:
discovery = await self.controller.find_ip_by_device_id(self.hkid)
self.finish_pairing = await discovery.start_pairing(self.hkid)
except aiohomekit.BusyError:

View File

@@ -3,8 +3,16 @@
"name": "HomeKit Controller",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["aiohomekit[IP]==0.2.49"],
"zeroconf": ["_hap._tcp.local."],
"after_dependencies": ["zeroconf"],
"codeowners": ["@Jc2k"]
"requirements": [
"aiohomekit==0.2.53"
],
"zeroconf": [
"_hap._tcp.local."
],
"after_dependencies": [
"zeroconf"
],
"codeowners": [
"@Jc2k"
]
}

View File

@@ -44,6 +44,7 @@
"already_configured": "Accessory is already configured with this controller.",
"invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.",
"accessory_not_found_error": "Cannot add pairing as device can no longer be found.",
"invalid_properties": "Invalid properties announced by device.",
"already_in_progress": "Config flow for device is already in progress."
}
}

View File

@@ -148,6 +148,12 @@ async def async_setup(hass, config):
discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config)
)
if not hass.data[DATA_KNX].xknx.devices:
_LOGGER.warning(
"No KNX devices are configured. Please read "
"https://www.home-assistant.io/blog/2020/09/17/release-115/#breaking-changes"
)
hass.services.async_register(
DOMAIN,
SERVICE_KNX_SEND,

View File

@@ -29,8 +29,15 @@ PLAYABLE_MEDIA_TYPES = [
MEDIA_TYPE_TRACK,
]
CONTENT_TYPE_MEDIA_CLASS = {
"library_music": MEDIA_CLASS_MUSIC,
CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = {
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW,
}
CHILD_TYPE_MEDIA_CLASS = {
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
@@ -151,8 +158,10 @@ async def build_item_response(media_library, payload):
except UnknownMediaType:
pass
return BrowseMedia(
media_class=CONTENT_TYPE_MEDIA_CLASS[search_type],
response = BrowseMedia(
media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get(
search_type, MEDIA_CLASS_DIRECTORY
),
media_content_id=search_id,
media_content_type=search_type,
title=title,
@@ -162,6 +171,13 @@ async def build_item_response(media_library, payload):
thumbnail=thumbnail,
)
if search_type == "library_music":
response.children_media_class = MEDIA_CLASS_MUSIC
else:
response.calculate_children_class()
return response
def item_payload(item, media_library):
"""
@@ -170,11 +186,12 @@ def item_payload(item, media_library):
Used by async_browse_media.
"""
title = item["label"]
thumbnail = item.get("thumbnail")
if thumbnail:
thumbnail = media_library.thumbnail_url(thumbnail)
media_class = None
if "songid" in item:
media_content_type = MEDIA_TYPE_TRACK
media_content_id = f"{item['songid']}"
@@ -213,16 +230,18 @@ def item_payload(item, media_library):
else:
# this case is for the top folder of each type
# possible content types: album, artist, movie, library_music, tvshow
media_class = MEDIA_CLASS_DIRECTORY
media_content_type = item["type"]
media_content_id = ""
can_play = False
can_expand = True
try:
media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type]
except KeyError as err:
_LOGGER.debug("Unknown media type received: %s", media_content_type)
raise UnknownMediaType from err
if media_class is None:
try:
media_class = CHILD_TYPE_MEDIA_CLASS[media_content_type]
except KeyError as err:
_LOGGER.debug("Unknown media type received: %s", media_content_type)
raise UnknownMediaType from err
return BrowseMedia(
title=title,

View File

@@ -116,6 +116,9 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
}
)
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": {CONF_NAME: self._name}})
try:
await validate_http(self.hass, self._get_data())
await validate_ws(self.hass, self._get_data())
@@ -129,8 +132,6 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
self.context.update({"title_placeholders": {CONF_NAME: self._name}})
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(self, user_input=None):

View File

@@ -2,7 +2,7 @@
"domain": "kodi",
"name": "Kodi",
"documentation": "https://www.home-assistant.io/integrations/kodi",
"requirements": ["pykodi==0.1.2"],
"requirements": ["pykodi==0.2.0"],
"codeowners": [
"@OnFreund"
],

View File

@@ -5,6 +5,7 @@ import logging
import re
import jsonrpc_base
from pykodi import CannotConnectError
import voluptuous as vol
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
@@ -324,11 +325,15 @@ class KodiEntity(MediaPlayerEntity):
self._app_properties["muted"] = data["muted"]
self.async_write_ha_state()
@callback
def async_on_quit(self, sender, data):
async def async_on_quit(self, sender, data):
"""Reset the player state on quit action."""
await self._clear_connection()
async def _clear_connection(self, close=True):
self._reset_state()
self.hass.async_create_task(self._connection.close())
self.async_write_ha_state()
if close:
await self._connection.close()
@property
def unique_id(self):
@@ -386,14 +391,23 @@ class KodiEntity(MediaPlayerEntity):
try:
await self._connection.connect()
self._on_ws_connected()
except jsonrpc_base.jsonrpc.TransportError:
_LOGGER.info("Unable to connect to Kodi via websocket")
except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError):
_LOGGER.debug("Unable to connect to Kodi via websocket", exc_info=True)
await self._clear_connection(False)
async def _ping(self):
try:
await self._kodi.ping()
except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError):
_LOGGER.debug("Unable to ping Kodi via websocket", exc_info=True)
await self._clear_connection()
async def _async_connect_websocket_if_disconnected(self, *_):
"""Reconnect the websocket if it fails."""
if not self._connection.connected:
await self._async_ws_connect()
else:
await self._ping()
@callback
def _register_ws_callbacks(self):
@@ -464,7 +478,7 @@ class KodiEntity(MediaPlayerEntity):
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
return (not self._connection.can_subscribe) or (not self._connection.connected)
return not self._connection.can_subscribe
@property
def volume_level(self):
@@ -700,7 +714,7 @@ class KodiEntity(MediaPlayerEntity):
_LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs)
result_ok = False
try:
result = self._kodi.call_method(method, **kwargs)
result = await self._kodi.call_method(method, **kwargs)
result_ok = True
except jsonrpc_base.jsonrpc.ProtocolError as exc:
result = exc.args[2]["error"]

View File

@@ -2,6 +2,8 @@
"domain": "loopenergy",
"name": "Loop Energy",
"documentation": "https://www.home-assistant.io/integrations/loopenergy",
"requirements": ["pyloopenergy==0.1.3"],
"codeowners": []
"requirements": ["pyloopenergy==0.2.1"],
"codeowners": [
"@pavoni"
]
}

View File

@@ -94,6 +94,7 @@ class LuciDeviceScanner(DeviceScanner):
last_results = []
for device in result:
last_results.append(device)
if device.reachable:
last_results.append(device)
self.last_results = last_results

View File

@@ -85,6 +85,7 @@ from .const import (
ATTR_SOUND_MODE,
ATTR_SOUND_MODE_LIST,
DOMAIN,
MEDIA_CLASS_DIRECTORY,
SERVICE_CLEAR_PLAYLIST,
SERVICE_PLAY_MEDIA,
SERVICE_SELECT_SOUND_MODE,
@@ -816,24 +817,10 @@ class MediaPlayerEntity(Entity):
media_content_type: Optional[str] = None,
media_content_id: Optional[str] = None,
) -> "BrowseMedia":
"""
Return a payload for the "media_player/browse_media" websocket command.
"""Return a BrowseMedia instance.
Payload should follow this format:
{
"title": str - Title of the item
"media_class": str - Media class
"media_content_type": str - see below
"media_content_id": str - see below
- Can be passed back in to browse further
- Can be used as-is with media_player.play_media service
"can_play": bool - If item is playable
"can_expand": bool - If item contains other media
"thumbnail": str (Optional) - URL to image thumbnail for item
"children": list (Optional) - [{<item_with_keys_above>}, ...]
}
Note: Children should omit the children key.
The BrowseMedia instance will be used by the
"media_player/browse_media" websocket command.
"""
raise NotImplementedError()
@@ -1054,6 +1041,7 @@ class BrowseMedia:
can_play: bool,
can_expand: bool,
children: Optional[List["BrowseMedia"]] = None,
children_media_class: Optional[str] = None,
thumbnail: Optional[str] = None,
):
"""Initialize browse media item."""
@@ -1064,10 +1052,14 @@ class BrowseMedia:
self.can_play = can_play
self.can_expand = can_expand
self.children = children
self.children_media_class = children_media_class
self.thumbnail = thumbnail
def as_dict(self, *, parent: bool = True) -> dict:
"""Convert Media class to browse media dictionary."""
if self.children_media_class is None:
self.calculate_children_class()
response = {
"title": self.title,
"media_class": self.media_class,
@@ -1075,6 +1067,7 @@ class BrowseMedia:
"media_content_id": self.media_content_id,
"can_play": self.can_play,
"can_expand": self.can_expand,
"children_media_class": self.children_media_class,
"thumbnail": self.thumbnail,
}
@@ -1089,3 +1082,14 @@ class BrowseMedia:
response["children"] = []
return response
def calculate_children_class(self) -> None:
"""Count the children media classes and calculate the correct class."""
if self.children is None or len(self.children) == 0:
return
self.children_media_class = MEDIA_CLASS_DIRECTORY
proposed_class = self.children[0].media_class
if all(child.media_class == proposed_class for child in self.children):
self.children_media_class = proposed_class

View File

@@ -31,10 +31,8 @@ DOMAIN = "media_player"
MEDIA_CLASS_ALBUM = "album"
MEDIA_CLASS_APP = "app"
MEDIA_CLASS_APPS = "apps"
MEDIA_CLASS_ARTIST = "artist"
MEDIA_CLASS_CHANNEL = "channel"
MEDIA_CLASS_CHANNELS = "channels"
MEDIA_CLASS_COMPOSER = "composer"
MEDIA_CLASS_CONTRIBUTING_ARTIST = "contributing_artist"
MEDIA_CLASS_DIRECTORY = "directory"

View File

@@ -1,7 +1,20 @@
"""Constants for the media_source integration."""
import re
from homeassistant.components.media_player.const import (
MEDIA_CLASS_IMAGE,
MEDIA_CLASS_MUSIC,
MEDIA_CLASS_VIDEO,
)
DOMAIN = "media_source"
MEDIA_MIME_TYPES = ("audio", "video", "image")
MEDIA_CLASS_MAP = {
"audio": MEDIA_CLASS_MUSIC,
"video": MEDIA_CLASS_VIDEO,
"image": MEDIA_CLASS_IMAGE,
}
URI_SCHEME = "media-source://"
URI_SCHEME_REGEX = re.compile(r"^media-source://(?P<domain>[^/]+)?(?P<identifier>.+)?")
URI_SCHEME_REGEX = re.compile(
r"^media-source:\/\/(?:(?P<domain>(?!.+__)(?!_)[\da-z_]+(?<!_))(?:\/(?P<identifier>(?!\/).+))?)?$"
)

View File

@@ -12,7 +12,7 @@ from homeassistant.components.media_source.error import Unresolvable
from homeassistant.core import HomeAssistant, callback
from homeassistant.util import sanitize_path
from .const import DOMAIN, MEDIA_MIME_TYPES
from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES
from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia
@@ -21,26 +21,7 @@ def async_setup(hass: HomeAssistant):
"""Set up local media source."""
source = LocalSource(hass)
hass.data[DOMAIN][DOMAIN] = source
hass.http.register_view(LocalMediaView(hass))
@callback
def async_parse_identifier(item: MediaSourceItem) -> Tuple[str, str]:
"""Parse identifier."""
if not item.identifier:
source_dir_id = "media"
location = ""
else:
source_dir_id, location = item.identifier.lstrip("/").split("/", 1)
if source_dir_id != "media":
raise Unresolvable("Unknown source directory.")
if location != sanitize_path(location):
raise Unresolvable("Invalid path.")
return source_dir_id, location
hass.http.register_view(LocalMediaView(hass, source))
class LocalSource(MediaSource):
@@ -56,22 +37,41 @@ class LocalSource(MediaSource):
@callback
def async_full_path(self, source_dir_id, location) -> Path:
"""Return full path."""
return self.hass.config.path("media", location)
return Path(self.hass.config.media_dirs[source_dir_id], location)
@callback
def async_parse_identifier(self, item: MediaSourceItem) -> Tuple[str, str]:
"""Parse identifier."""
if not item.identifier:
# Empty source_dir_id and location
return "", ""
source_dir_id, location = item.identifier.split("/", 1)
if source_dir_id not in self.hass.config.media_dirs:
raise Unresolvable("Unknown source directory.")
if location != sanitize_path(location):
raise Unresolvable("Invalid path.")
return source_dir_id, location
async def async_resolve_media(self, item: MediaSourceItem) -> str:
"""Resolve media to a url."""
source_dir_id, location = async_parse_identifier(item)
source_dir_id, location = self.async_parse_identifier(item)
if source_dir_id == "" or source_dir_id not in self.hass.config.media_dirs:
raise Unresolvable("Unknown source directory.")
mime_type, _ = mimetypes.guess_type(
self.async_full_path(source_dir_id, location)
str(self.async_full_path(source_dir_id, location))
)
return PlayMedia(item.identifier, mime_type)
return PlayMedia(f"/media/{item.identifier}", mime_type)
async def async_browse_media(
self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
) -> BrowseMediaSource:
"""Return media."""
try:
source_dir_id, location = async_parse_identifier(item)
source_dir_id, location = self.async_parse_identifier(item)
except Unresolvable as err:
raise BrowseError(str(err)) from err
@@ -79,9 +79,37 @@ class LocalSource(MediaSource):
self._browse_media, source_dir_id, location
)
def _browse_media(self, source_dir_id, location):
def _browse_media(self, source_dir_id: str, location: Path):
"""Browse media."""
full_path = Path(self.hass.config.path("media", location))
# If only one media dir is configured, use that as the local media root
if source_dir_id == "" and len(self.hass.config.media_dirs) == 1:
source_dir_id = list(self.hass.config.media_dirs)[0]
# Multiple folder, root is requested
if source_dir_id == "":
if location:
raise BrowseError("Folder not found.")
base = BrowseMediaSource(
domain=DOMAIN,
identifier="",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_type=None,
title=self.name,
can_play=False,
can_expand=True,
children_media_class=MEDIA_CLASS_DIRECTORY,
)
base.children = [
self._browse_media(source_dir_id, "")
for source_dir_id in self.hass.config.media_dirs
]
return base
full_path = Path(self.hass.config.media_dirs[source_dir_id], location)
if not full_path.exists():
if location == "":
@@ -112,11 +140,15 @@ class LocalSource(MediaSource):
if is_dir:
title += "/"
media_class = MEDIA_CLASS_MAP.get(
mime_type and mime_type.split("/")[0], MEDIA_CLASS_DIRECTORY
)
media = BrowseMediaSource(
domain=DOMAIN,
identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_type="directory",
identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.media_dirs[source_dir_id])}",
media_class=media_class,
media_content_type=mime_type or "",
title=title,
can_play=is_file,
can_expand=is_dir,
@@ -132,6 +164,9 @@ class LocalSource(MediaSource):
if child:
media.children.append(child)
# Sort children showing directories first, then by name
media.children.sort(key=lambda child: (child.can_play, child.title))
return media
@@ -142,19 +177,25 @@ class LocalMediaView(HomeAssistantView):
Returns media files in config/media.
"""
url = "/media/{location:.*}"
url = "/media/{source_dir_id}/{location:.*}"
name = "media"
def __init__(self, hass: HomeAssistant):
def __init__(self, hass: HomeAssistant, source: LocalSource):
"""Initialize the media view."""
self.hass = hass
self.source = source
async def get(self, request: web.Request, location: str) -> web.FileResponse:
async def get(
self, request: web.Request, source_dir_id: str, location: str
) -> web.FileResponse:
"""Start a GET request."""
if location != sanitize_path(location):
return web.HTTPNotFound()
raise web.HTTPNotFound()
media_path = Path(self.hass.config.path("media", location))
if source_dir_id not in self.hass.config.media_dirs:
raise web.HTTPNotFound()
media_path = self.source.async_full_path(source_dir_id, location)
# Check that the file exists
if not media_path.is_file():

View File

@@ -6,7 +6,7 @@ from typing import List, Optional, Tuple
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import (
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_CHANNELS,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
)
@@ -54,11 +54,12 @@ class MediaSourceItem:
base = BrowseMediaSource(
domain=None,
identifier=None,
media_class=MEDIA_CLASS_CHANNELS,
media_class=MEDIA_CLASS_DIRECTORY,
media_content_type=MEDIA_TYPE_CHANNELS,
title="Media Sources",
can_play=False,
can_expand=True,
children_media_class=MEDIA_CLASS_CHANNEL,
)
base.children = [
BrowseMediaSource(

View File

@@ -104,7 +104,6 @@ class MetWeather(CoordinatorEntity, WeatherEntity):
self._config = config
self._is_metric = is_metric
self._hourly = hourly
self._name_appendix = "-hourly" if hourly else ""
@property
def track_home(self):
@@ -114,23 +113,34 @@ class MetWeather(CoordinatorEntity, WeatherEntity):
@property
def unique_id(self):
"""Return unique ID."""
name_appendix = ""
if self._hourly:
name_appendix = "-hourly"
if self.track_home:
return f"home{self._name_appendix}"
return f"home{name_appendix}"
return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{self._name_appendix}"
return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{name_appendix}"
@property
def name(self):
"""Return the name of the sensor."""
name = self._config.get(CONF_NAME)
name_appendix = ""
if self._hourly:
name_appendix = " Hourly"
if name is not None:
return f"{name}{self._name_appendix}"
return f"{name}{name_appendix}"
if self.track_home:
return f"{self.hass.config.location_name}{self._name_appendix}"
return f"{self.hass.config.location_name}{name_appendix}"
return f"{DEFAULT_NAME}{self._name_appendix}"
return f"{DEFAULT_NAME}{name_appendix}"
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return not self._hourly
@property
def condition(self):

View File

@@ -79,9 +79,10 @@ class MeteoFranceSensor(CoordinatorEntity):
"""Initialize the Meteo-France sensor."""
super().__init__(coordinator)
self._type = sensor_type
city_name = self.coordinator.data.position["name"]
self._name = f"{city_name} {SENSOR_TYPES[self._type][ENTITY_NAME]}"
self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}_{self._type}"
if hasattr(self.coordinator.data, "position"):
city_name = self.coordinator.data.position["name"]
self._name = f"{city_name} {SENSOR_TYPES[self._type][ENTITY_NAME]}"
self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}_{self._type}"
@property
def unique_id(self):

View File

@@ -171,7 +171,7 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
return
self._available = True
return bool(result.bits[0])
return bool(result.bits[coil])
def _write_coil(self, coil, value):
"""Write coil using the Modbus hub slave."""

View File

@@ -138,12 +138,12 @@ class MpdDevice(MediaPlayerEntity):
if position is None:
position = self._status.get("time")
if position is not None and ":" in position:
if isinstance(position, str) and ":" in position:
position = position.split(":")[0]
if position is not None and self._media_position != position:
self._media_position_updated_at = dt_util.utcnow()
self._media_position = int(position)
self._media_position = int(float(position))
self._update_playlists()
@@ -159,8 +159,9 @@ class MpdDevice(MediaPlayerEntity):
self._connect()
self._fetch_status()
except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError):
except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError) as error:
# Cleanly disconnect in case connection is not in valid state
_LOGGER.debug("Error updating status: %s", error)
self._disconnect()
@property

View File

@@ -60,6 +60,7 @@ from .const import (
CONF_RETAIN,
CONF_STATE_TOPIC,
CONF_WILL_MESSAGE,
DATA_MQTT_CONFIG,
DEFAULT_BIRTH,
DEFAULT_DISCOVERY,
DEFAULT_PAYLOAD_AVAILABLE,
@@ -88,7 +89,6 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "mqtt"
DATA_MQTT = "mqtt"
DATA_MQTT_CONFIG = "mqtt_config"
SERVICE_PUBLISH = "publish"
SERVICE_DUMP = "dump"
@@ -134,7 +134,7 @@ CONNECTION_FAILED = "connection_failed"
CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable"
DISCOVERY_COOLDOWN = 2
TIMEOUT_ACK = 1
TIMEOUT_ACK = 10
PLATFORMS = [
"alarm_control_panel",
@@ -1305,7 +1305,7 @@ class MqttDiscoveryUpdate(Entity):
debug_info.add_entity_discovery_data(
self.hass, self._discovery_data, self.entity_id
)
# Set in case the entity has been removed and is re-added
# Set in case the entity has been removed and is re-added, for example when changing entity_id
set_discovery_hash(self.hass, discovery_hash)
self._remove_signal = async_dispatcher_connect(
self.hass,

View File

@@ -104,7 +104,7 @@ async def async_setup_platform(
):
"""Set up MQTT alarm control panel through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities)
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -116,7 +116,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -128,10 +128,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_data=None
hass, config, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up the MQTT Alarm Control Panel platform."""
async_add_entities([MqttAlarm(config, config_entry, discovery_data)])
async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)])
class MqttAlarm(
@@ -143,13 +143,16 @@ class MqttAlarm(
):
"""Representation of a MQTT alarm status."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Init the MQTT Alarm Control Panel."""
self.hass = hass
self._state = None
self._config = config
self._unique_id = config.get(CONF_UNIQUE_ID)
self._sub_state = None
# Load config
self._setup_from_config(config)
device_config = config.get(CONF_DEVICE)
MqttAttributes.__init__(self, config)
@@ -165,26 +168,30 @@ class MqttAlarm(
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._config = config
self._setup_from_config(config)
await self.attributes_discovery_update(config)
await self.availability_discovery_update(config)
await self.device_info_discovery_update(config)
await self._subscribe_topics()
self.async_write_ha_state()
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
def _setup_from_config(self, config):
self._config = config
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = self.hass
command_template = self._config[CONF_COMMAND_TEMPLATE]
command_template.hass = self.hass
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@callback
@log_messages(self.hass, self.entity_id)
def message_received(msg):
"""Run when new MQTT message has been received."""
payload = msg.payload
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
payload = value_template.async_render_with_possible_json_value(
msg.payload, self._state

View File

@@ -76,7 +76,7 @@ async def async_setup_platform(
):
"""Set up MQTT binary sensor through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities)
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -88,7 +88,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -100,10 +100,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_data=None
hass, config, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up the MQTT binary sensor."""
async_add_entities([MqttBinarySensor(config, config_entry, discovery_data)])
async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)])
class MqttBinarySensor(
@@ -115,9 +115,9 @@ class MqttBinarySensor(
):
"""Representation a binary sensor that is updated by MQTT."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT binary sensor."""
self._config = config
self.hass = hass
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = None
self._sub_state = None
@@ -128,6 +128,10 @@ class MqttBinarySensor(
self._expired = True
else:
self._expired = None
# Load config
self._setup_from_config(config)
device_config = config.get(CONF_DEVICE)
MqttAttributes.__init__(self, config)
@@ -143,19 +147,22 @@ class MqttBinarySensor(
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._config = config
self._setup_from_config(config)
await self.attributes_discovery_update(config)
await self.availability_discovery_update(config)
await self.device_info_discovery_update(config)
await self._subscribe_topics()
self.async_write_ha_state()
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
def _setup_from_config(self, config):
self._config = config
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = self.hass
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@callback
def off_delay_listener(now):
"""Switch device off after a delay."""

View File

@@ -24,6 +24,7 @@ from .const import (
CONF_BROKER,
CONF_DISCOVERY,
CONF_WILL_MESSAGE,
DATA_MQTT_CONFIG,
DEFAULT_BIRTH,
DEFAULT_DISCOVERY,
DEFAULT_WILL,
@@ -162,6 +163,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
"""Manage the MQTT options."""
errors = {}
current_config = self.config_entry.data
yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {})
if user_input is not None:
can_connect = await self.hass.async_add_executor_job(
try_connection,
@@ -178,20 +180,22 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
errors["base"] = "cannot_connect"
fields = OrderedDict()
fields[vol.Required(CONF_BROKER, default=current_config[CONF_BROKER])] = str
fields[vol.Required(CONF_PORT, default=current_config[CONF_PORT])] = vol.Coerce(
int
)
current_broker = current_config.get(CONF_BROKER, yaml_config.get(CONF_BROKER))
current_port = current_config.get(CONF_PORT, yaml_config.get(CONF_PORT))
current_user = current_config.get(CONF_USERNAME, yaml_config.get(CONF_USERNAME))
current_pass = current_config.get(CONF_PASSWORD, yaml_config.get(CONF_PASSWORD))
fields[vol.Required(CONF_BROKER, default=current_broker)] = str
fields[vol.Required(CONF_PORT, default=current_port)] = vol.Coerce(int)
fields[
vol.Optional(
CONF_USERNAME,
description={"suggested_value": current_config.get(CONF_USERNAME)},
description={"suggested_value": current_user},
)
] = str
fields[
vol.Optional(
CONF_PASSWORD,
description={"suggested_value": current_config.get(CONF_PASSWORD)},
description={"suggested_value": current_pass},
)
] = str
@@ -205,6 +209,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
"""Manage the MQTT options."""
errors = {}
current_config = self.config_entry.data
yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {})
options_config = {}
if user_input is not None:
bad_birth = False
@@ -253,16 +258,24 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
)
return self.async_create_entry(title="", data=None)
birth = {**DEFAULT_BIRTH, **current_config.get(CONF_BIRTH_MESSAGE, {})}
will = {**DEFAULT_WILL, **current_config.get(CONF_WILL_MESSAGE, {})}
birth = {
**DEFAULT_BIRTH,
**current_config.get(
CONF_BIRTH_MESSAGE, yaml_config.get(CONF_BIRTH_MESSAGE, {})
),
}
will = {
**DEFAULT_WILL,
**current_config.get(
CONF_WILL_MESSAGE, yaml_config.get(CONF_WILL_MESSAGE, {})
),
}
discovery = current_config.get(
CONF_DISCOVERY, yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY)
)
fields = OrderedDict()
fields[
vol.Optional(
CONF_DISCOVERY,
default=current_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY),
)
] = bool
fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = bool
# Birth message is disabled if CONF_BIRTH_MESSAGE = {}
fields[

View File

@@ -17,6 +17,8 @@ CONF_RETAIN = ATTR_RETAIN
CONF_STATE_TOPIC = "state_topic"
CONF_WILL_MESSAGE = "will_message"
DATA_MQTT_CONFIG = "mqtt_config"
DEFAULT_PREFIX = "homeassistant"
DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status"
DEFAULT_DISCOVERY = False

View File

@@ -174,7 +174,7 @@ async def async_setup_platform(
):
"""Set up MQTT cover through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities)
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -186,7 +186,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -198,10 +198,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_data=None
hass, config, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up the MQTT Cover."""
async_add_entities([MqttCover(config, config_entry, discovery_data)])
async_add_entities([MqttCover(hass, config, config_entry, discovery_data)])
class MqttCover(
@@ -213,8 +213,9 @@ class MqttCover(
):
"""Representation of a cover that can be controlled using MQTT."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the cover."""
self.hass = hass
self._unique_id = config.get(CONF_UNIQUE_ID)
self._position = None
self._state = None
@@ -257,8 +258,6 @@ class MqttCover(
)
self._tilt_optimistic = config[CONF_TILT_STATE_OPTIMISTIC]
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
template.hass = self.hass
@@ -269,6 +268,8 @@ class MqttCover(
if tilt_status_template is not None:
tilt_status_template.hass = self.hass
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
topics = {}
@callback
@@ -276,6 +277,7 @@ class MqttCover(
def tilt_message_received(msg):
"""Handle tilt updates."""
payload = msg.payload
tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE)
if tilt_status_template is not None:
payload = tilt_status_template.async_render_with_possible_json_value(
payload
@@ -296,6 +298,7 @@ class MqttCover(
def state_message_received(msg):
"""Handle new MQTT state messages."""
payload = msg.payload
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
payload = template.async_render_with_possible_json_value(payload)
@@ -321,6 +324,7 @@ class MqttCover(
def position_message_received(msg):
"""Handle new MQTT state messages."""
payload = msg.payload
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
payload = template.async_render_with_possible_json_value(payload)

View File

@@ -115,7 +115,7 @@ async def async_setup_platform(
):
"""Set up MQTT fan through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities)
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -127,7 +127,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -139,10 +139,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_data=None
hass, config, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up the MQTT fan."""
async_add_entities([MqttFan(config, config_entry, discovery_data)])
async_add_entities([MqttFan(hass, config, config_entry, discovery_data)])
class MqttFan(
@@ -154,8 +154,9 @@ class MqttFan(
):
"""A MQTT fan component."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT fan."""
self.hass = hass
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = False
self._speed = None
@@ -242,22 +243,22 @@ class MqttFan(
self._topic[CONF_SPEED_COMMAND_TOPIC] is not None and SUPPORT_SET_SPEED
)
for key, tpl in list(self._templates.items()):
if tpl is None:
self._templates[key] = lambda value: value
else:
tpl.hass = self.hass
self._templates[key] = tpl.async_render_with_possible_json_value
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
topics = {}
templates = {}
for key, tpl in list(self._templates.items()):
if tpl is None:
templates[key] = lambda value: value
else:
tpl.hass = self.hass
templates[key] = tpl.async_render_with_possible_json_value
@callback
@log_messages(self.hass, self.entity_id)
def state_received(msg):
"""Handle new received MQTT message."""
payload = templates[CONF_STATE](msg.payload)
payload = self._templates[CONF_STATE](msg.payload)
if payload == self._payload["STATE_ON"]:
self._state = True
elif payload == self._payload["STATE_OFF"]:
@@ -275,7 +276,7 @@ class MqttFan(
@log_messages(self.hass, self.entity_id)
def speed_received(msg):
"""Handle new received MQTT message for the speed."""
payload = templates[ATTR_SPEED](msg.payload)
payload = self._templates[ATTR_SPEED](msg.payload)
if payload == self._payload["SPEED_LOW"]:
self._speed = SPEED_LOW
elif payload == self._payload["SPEED_MEDIUM"]:
@@ -298,7 +299,7 @@ class MqttFan(
@log_messages(self.hass, self.entity_id)
def oscillation_received(msg):
"""Handle new received MQTT message for the oscillation."""
payload = templates[OSCILLATION](msg.payload)
payload = self._templates[OSCILLATION](msg.payload)
if payload == self._payload["OSCILLATE_ON_PAYLOAD"]:
self._oscillation = True
elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]:

View File

@@ -254,7 +254,7 @@ class MqttLight(
value_templates = {}
for key in VALUE_TEMPLATE_KEYS:
value_templates[key] = lambda value: value
value_templates[key] = lambda value, _: value
for key in VALUE_TEMPLATE_KEYS & config.keys():
tpl = config[key]
value_templates[key] = tpl.async_render_with_possible_json_value
@@ -304,7 +304,9 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def state_received(msg):
"""Handle new MQTT messages."""
payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE](
msg.payload, None
)
if not payload:
_LOGGER.debug("Ignoring empty state message from '%s'", msg.topic)
return
@@ -328,7 +330,9 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def brightness_received(msg):
"""Handle new MQTT messages for the brightness."""
payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE](
msg.payload, None
)
if not payload:
_LOGGER.debug("Ignoring empty brightness message from '%s'", msg.topic)
return
@@ -360,7 +364,7 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def rgb_received(msg):
"""Handle new MQTT messages for RGB."""
payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload, None)
if not payload:
_LOGGER.debug("Ignoring empty rgb message from '%s'", msg.topic)
return
@@ -392,7 +396,9 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def color_temp_received(msg):
"""Handle new MQTT messages for color temperature."""
payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE](
msg.payload, None
)
if not payload:
_LOGGER.debug("Ignoring empty color temp message from '%s'", msg.topic)
return
@@ -422,7 +428,9 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def effect_received(msg):
"""Handle new MQTT messages for effect."""
payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE](
msg.payload, None
)
if not payload:
_LOGGER.debug("Ignoring empty effect message from '%s'", msg.topic)
return
@@ -452,7 +460,7 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def hs_received(msg):
"""Handle new MQTT messages for hs color."""
payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload, None)
if not payload:
_LOGGER.debug("Ignoring empty hs message from '%s'", msg.topic)
return
@@ -484,7 +492,9 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def white_value_received(msg):
"""Handle new MQTT messages for white value."""
payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE](
msg.payload, None
)
if not payload:
_LOGGER.debug("Ignoring empty white value message from '%s'", msg.topic)
return
@@ -516,7 +526,7 @@ class MqttLight(
@log_messages(self.hass, self.entity_id)
def xy_received(msg):
"""Handle new MQTT messages for xy color."""
payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload)
payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload, None)
if not payload:
_LOGGER.debug("Ignoring empty xy-color message from '%s'", msg.topic)
return

View File

@@ -77,7 +77,7 @@ async def async_setup_platform(
):
"""Set up MQTT lock panel through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities)
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -89,7 +89,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -101,10 +101,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_data=None
hass, config, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up the MQTT Lock platform."""
async_add_entities([MqttLock(config, config_entry, discovery_data)])
async_add_entities([MqttLock(hass, config, config_entry, discovery_data)])
class MqttLock(
@@ -116,8 +116,9 @@ class MqttLock(
):
"""Representation of a lock that can be toggled using MQTT."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the lock."""
self.hass = hass
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = False
self._sub_state = None
@@ -154,17 +155,19 @@ class MqttLock(
self._optimistic = config[CONF_OPTIMISTIC]
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = self.hass
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@callback
@log_messages(self.hass, self.entity_id)
def message_received(msg):
"""Handle new MQTT messages."""
payload = msg.payload
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
payload = value_template.async_render_with_possible_json_value(payload)
if payload == self._config[CONF_STATE_LOCKED]:

View File

@@ -70,7 +70,7 @@ async def async_setup_platform(
):
"""Set up MQTT sensors through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities)
await _async_setup_entity(hass, config, async_add_entities)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -82,7 +82,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -94,10 +94,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config: ConfigType, async_add_entities, config_entry=None, discovery_data=None
hass, config: ConfigType, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up MQTT sensor."""
async_add_entities([MqttSensor(config, config_entry, discovery_data)])
async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)])
class MqttSensor(
@@ -105,9 +105,9 @@ class MqttSensor(
):
"""Representation of a sensor that can be updated using MQTT."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the sensor."""
self._config = config
self.hass = hass
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = None
self._sub_state = None
@@ -118,6 +118,10 @@ class MqttSensor(
self._expired = True
else:
self._expired = None
# Load config
self._setup_from_config(config)
device_config = config.get(CONF_DEVICE)
MqttAttributes.__init__(self, config)
@@ -133,19 +137,23 @@ class MqttSensor(
async def discovery_update(self, discovery_payload):
"""Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload)
self._config = config
self._setup_from_config(config)
await self.attributes_discovery_update(config)
await self.availability_discovery_update(config)
await self.device_info_discovery_update(config)
await self._subscribe_topics()
self.async_write_ha_state()
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
def _setup_from_config(self, config):
"""(Re)Setup the entity."""
self._config = config
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
template.hass = self.hass
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@callback
@log_messages(self.hass, self.entity_id)
def message_received(msg):
@@ -169,6 +177,7 @@ class MqttSensor(
self.hass, self._value_is_expired, expiration_at
)
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
payload = template.async_render_with_possible_json_value(
payload, self._state

View File

@@ -73,7 +73,7 @@ async def async_setup_platform(
):
"""Set up MQTT switch through configuration.yaml."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
await _async_setup_entity(config, async_add_entities, discovery_info)
await _async_setup_entity(hass, config, async_add_entities, discovery_info)
async def async_setup_entry(hass, config_entry, async_add_entities):
@@ -85,7 +85,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
try:
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_data
hass, config, async_add_entities, config_entry, discovery_data
)
except Exception:
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
@@ -97,10 +97,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def _async_setup_entity(
config, async_add_entities, config_entry=None, discovery_data=None
hass, config, async_add_entities, config_entry=None, discovery_data=None
):
"""Set up the MQTT switch."""
async_add_entities([MqttSwitch(config, config_entry, discovery_data)])
async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)])
class MqttSwitch(
@@ -113,8 +113,9 @@ class MqttSwitch(
):
"""Representation of a switch that can be toggled using MQTT."""
def __init__(self, config, config_entry, discovery_data):
def __init__(self, hass, config, config_entry, discovery_data):
"""Initialize the MQTT switch."""
self.hass = hass
self._state = False
self._sub_state = None
@@ -160,17 +161,19 @@ class MqttSwitch(
self._optimistic = config[CONF_OPTIMISTIC]
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
template.hass = self.hass
async def _subscribe_topics(self):
"""(Re)Subscribe to topics."""
@callback
@log_messages(self.hass, self.entity_id)
def state_message_received(msg):
"""Handle new MQTT state messages."""
payload = msg.payload
template = self._config.get(CONF_VALUE_TEMPLATE)
if template is not None:
payload = template.async_render_with_possible_json_value(payload)
if payload == self._state_on:

View File

@@ -284,9 +284,9 @@ class NetatmoCamera(NetatmoBase, Camera):
self._data.events.get(self._id, {})
)
elif self._model == "NOC": # Smart Outdoor Camera
self.hass.data[DOMAIN][DATA_EVENTS][
self._id
] = self._data.outdoor_events.get(self._id, {})
self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events(
self._data.outdoor_events.get(self._id, {})
)
def process_events(self, events):
"""Add meta data to events."""

View File

@@ -5,6 +5,7 @@ import re
from typing import Optional, Tuple
from homeassistant.components.media_player.const import (
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_VIDEO,
MEDIA_TYPE_VIDEO,
)
@@ -79,8 +80,20 @@ class NetatmoSource(MediaSource):
) -> BrowseMediaSource:
if event_id and event_id in self.events[camera_id]:
created = dt.datetime.fromtimestamp(event_id)
thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url")
message = remove_html_tags(self.events[camera_id][event_id]["message"])
if self.events[camera_id][event_id]["type"] == "outdoor":
thumbnail = (
self.events[camera_id][event_id]["event_list"][0]
.get("snapshot", {})
.get("url")
)
message = remove_html_tags(
self.events[camera_id][event_id]["event_list"][0]["message"]
)
else:
thumbnail = (
self.events[camera_id][event_id].get("snapshot", {}).get("url")
)
message = remove_html_tags(self.events[camera_id][event_id]["message"])
title = f"{created} - {message}"
else:
title = self.hass.data[DOMAIN][DATA_CAMERAS].get(camera_id, MANUFACTURER)
@@ -91,10 +104,12 @@ class NetatmoSource(MediaSource):
else:
path = f"{source}/{camera_id}"
media_class = MEDIA_CLASS_DIRECTORY if event_id is None else MEDIA_CLASS_VIDEO
media = BrowseMediaSource(
domain=DOMAIN,
identifier=path,
media_class=MEDIA_CLASS_VIDEO,
media_class=media_class,
media_content_type=MEDIA_TYPE_VIDEO,
title=title,
can_play=bool(

View File

@@ -8,7 +8,8 @@
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]"
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
@@ -39,4 +40,4 @@
}
}
}
}
}

View File

@@ -100,7 +100,6 @@ def setup(hass, config):
_LOGGER.error("Nextcloud setup failed - Check configuration")
hass.data[DOMAIN] = get_data_points(ncm.data)
hass.data[DOMAIN]["instance"] = conf[CONF_URL]
def nextcloud_update(event_time):
"""Update data from nextcloud api."""
@@ -111,6 +110,7 @@ def setup(hass, config):
return False
hass.data[DOMAIN] = get_data_points(ncm.data)
hass.data[DOMAIN]["instance"] = conf[CONF_URL]
# Update sensors on time interval
track_time_interval(hass, nextcloud_update, conf[CONF_SCAN_INTERVAL])

View File

@@ -38,8 +38,8 @@ def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]:
"""
nzbget_api = NZBGetAPI(
data[CONF_HOST],
data[CONF_USERNAME] if data[CONF_USERNAME] != "" else None,
data[CONF_PASSWORD] if data[CONF_PASSWORD] != "" else None,
data.get(CONF_USERNAME),
data.get(CONF_PASSWORD),
data[CONF_SSL],
data[CONF_VERIFY_SSL],
data[CONF_PORT],

View File

@@ -29,8 +29,8 @@ class NZBGetDataUpdateCoordinator(DataUpdateCoordinator):
"""Initialize global NZBGet data updater."""
self.nzbget = NZBGetAPI(
config[CONF_HOST],
config[CONF_USERNAME] if config[CONF_USERNAME] != "" else None,
config[CONF_PASSWORD] if config[CONF_PASSWORD] != "" else None,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
config[CONF_SSL],
config[CONF_VERIFY_SSL],
config[CONF_PORT],

View File

@@ -91,7 +91,7 @@ class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if CONF_LONGITUDE not in config:
config[CONF_LONGITUDE] = self.hass.config.longitude
if CONF_MODE not in config:
config[CONF_MODE] = DEFAULT_LANGUAGE
config[CONF_MODE] = DEFAULT_FORECAST_MODE
if CONF_LANGUAGE not in config:
config[CONF_LANGUAGE] = DEFAULT_LANGUAGE
return await self.async_step_user(config)

View File

@@ -12,7 +12,7 @@ from homeassistant.components.media_player import (
)
from homeassistant.components.media_player.const import (
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_CHANNELS,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
SUPPORT_BROWSE_MEDIA,
@@ -290,7 +290,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
return BrowseMedia(
title="Channels",
media_class=MEDIA_CLASS_CHANNELS,
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="",
media_content_type=MEDIA_TYPE_CHANNELS,
can_play=False,

View File

@@ -1,4 +1,28 @@
"""The ping component."""
from homeassistant.core import callback
DOMAIN = "ping"
PLATFORMS = ["binary_sensor"]
PING_ID = "ping_id"
DEFAULT_START_ID = 129
MAX_PING_ID = 65534
@callback
def async_get_next_ping_id(hass):
"""Find the next id to use in the outbound ping.
Must be called in async
"""
current_id = hass.data.setdefault(DOMAIN, {}).get(PING_ID, DEFAULT_START_ID)
if current_id == MAX_PING_ID:
next_id = DEFAULT_START_ID
else:
next_id = current_id + 1
hass.data[DOMAIN][PING_ID] = next_id
return next_id

View File

@@ -1,6 +1,7 @@
"""Tracks the latency of a host by sending ICMP echo requests (ping)."""
import asyncio
from datetime import timedelta
from functools import partial
import logging
import re
import sys
@@ -14,7 +15,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import setup_reload_service
from . import DOMAIN, PLATFORMS
from . import DOMAIN, PLATFORMS, async_get_next_ping_id
from .const import PING_TIMEOUT
_LOGGER = logging.getLogger(__name__)
@@ -131,20 +132,28 @@ class PingData:
class PingDataICMPLib(PingData):
"""The Class for handling the data retrieval using icmplib."""
def ping(self):
"""Send ICMP echo request and return details."""
return icmp_ping(self._ip_address, count=self._count)
async def async_update(self) -> None:
"""Retrieve the latest details from the host."""
data = await self.hass.async_add_executor_job(self.ping)
_LOGGER.debug("ping address: %s", self._ip_address)
data = await self.hass.async_add_executor_job(
partial(
icmp_ping,
self._ip_address,
count=self._count,
id=async_get_next_ping_id(self.hass),
)
)
self.available = data.is_alive
if not self.available:
self.data = False
return
self.data = {
"min": data.min_rtt,
"max": data.max_rtt,
"avg": data.avg_rtt,
"mdev": "",
}
self.available = data.is_alive
class PingDataSubProcess(PingData):
@@ -201,7 +210,8 @@ class PingDataSubProcess(PingData):
out_error,
)
if pinger.returncode != 0:
if pinger.returncode > 1:
# returncode of 1 means the host is unreachable
_LOGGER.exception(
"Error running command: `%s`, return code: %s",
" ".join(self._ping_cmd),

View File

@@ -15,8 +15,10 @@ from homeassistant.components.device_tracker.const import (
SOURCE_TYPE_ROUTER,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.async_ import run_callback_threadsafe
from homeassistant.util.process import kill_subprocess
from . import async_get_next_ping_id
from .const import PING_ATTEMPTS_COUNT, PING_TIMEOUT
_LOGGER = logging.getLogger(__name__)
@@ -76,15 +78,22 @@ class HostSubProcess:
class HostICMPLib:
"""Host object with ping detection."""
def __init__(self, ip_address, dev_id, _, config):
def __init__(self, ip_address, dev_id, hass, config):
"""Initialize the Host pinger."""
self.hass = hass
self.ip_address = ip_address
self.dev_id = dev_id
self._count = config[CONF_PING_COUNT]
def ping(self):
"""Send an ICMP echo request and return True if success."""
return icmp_ping(self.ip_address, count=PING_ATTEMPTS_COUNT).is_alive
next_id = run_callback_threadsafe(
self.hass.loop, async_get_next_ping_id, self.hass
).result()
return icmp_ping(
self.ip_address, count=PING_ATTEMPTS_COUNT, id=next_id
).is_alive
def update(self, see):
"""Update device state by sending one or more ping messages."""

View File

@@ -3,6 +3,6 @@
"name": "Ping (ICMP)",
"documentation": "https://www.home-assistant.io/integrations/ping",
"codeowners": [],
"requirements": ["icmplib==1.1.1"],
"requirements": ["icmplib==1.1.3"],
"quality_scale": "internal"
}

View File

@@ -26,7 +26,7 @@ class UnknownMediaType(BrowseError):
EXPANDABLES = ["album", "artist", "playlist", "season", "show"]
PLAYLISTS_BROWSE_PAYLOAD = {
"title": "Playlists",
"media_class": MEDIA_CLASS_PLAYLIST,
"media_class": MEDIA_CLASS_DIRECTORY,
"media_content_id": "all",
"media_content_type": "playlists",
"can_play": False,
@@ -94,10 +94,21 @@ def browse_media(
if special_folder:
if media_content_type == "server":
library_or_section = plex_server.library
children_media_class = MEDIA_CLASS_DIRECTORY
title = plex_server.friendly_name
elif media_content_type == "library":
library_or_section = plex_server.library.sectionByID(media_content_id)
title = library_or_section.title
try:
children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE]
except KeyError as err:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
) from err
else:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
)
payload = {
"title": title,
@@ -107,6 +118,7 @@ def browse_media(
"can_play": False,
"can_expand": True,
"children": [],
"children_media_class": children_media_class,
}
method = SPECIAL_METHODS[special_folder]
@@ -116,13 +128,20 @@ def browse_media(
payload["children"].append(item_payload(item))
except UnknownMediaType:
continue
return BrowseMedia(**payload)
if media_content_type in ["server", None]:
return server_payload(plex_server)
try:
if media_content_type in ["server", None]:
return server_payload(plex_server)
if media_content_type == "library":
return library_payload(plex_server, media_content_id)
if media_content_type == "library":
return library_payload(plex_server, media_content_id)
except UnknownMediaType as err:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
) from err
if media_content_type == "playlists":
return playlists_payload(plex_server)
@@ -160,6 +179,11 @@ def item_payload(item):
def library_section_payload(section):
"""Create response payload for a single library section."""
try:
children_media_class = ITEM_TYPE_MEDIA_CLASS[section.TYPE]
except KeyError as err:
_LOGGER.debug("Unknown type received: %s", section.TYPE)
raise UnknownMediaType from err
return BrowseMedia(
title=section.title,
media_class=MEDIA_CLASS_DIRECTORY,
@@ -167,6 +191,7 @@ def library_section_payload(section):
media_content_type="library",
can_play=False,
can_expand=True,
children_media_class=children_media_class,
)
@@ -180,6 +205,7 @@ def special_library_payload(parent_payload, special_type):
media_content_type=parent_payload.media_content_type,
can_play=False,
can_expand=True,
children_media_class=parent_payload.children_media_class,
)
@@ -194,6 +220,7 @@ def server_payload(plex_server):
can_expand=True,
)
server_info.children = []
server_info.children_media_class = MEDIA_CLASS_DIRECTORY
server_info.children.append(special_library_payload(server_info, "On Deck"))
server_info.children.append(special_library_payload(server_info, "Recently Added"))
for library in plex_server.library.sections():
@@ -229,4 +256,6 @@ def playlists_payload(plex_server):
playlists_info["children"].append(item_payload(playlist))
except UnknownMediaType:
continue
return BrowseMedia(**playlists_info)
response = BrowseMedia(**playlists_info)
response.children_media_class = MEDIA_CLASS_PLAYLIST
return response

View File

@@ -94,6 +94,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api.get_all_devices()
if entry.unique_id is None:
if api.smile_version[0] != "1.8.0":
hass.config_entries.async_update_entry(entry, unique_id=api.smile_hostname)
undo_listener = entry.add_update_listener(_update_listener)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {

View File

@@ -96,6 +96,10 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if self.discovery_info:
user_input[CONF_HOST] = self.discovery_info[CONF_HOST]
for entry in self._async_current_entries():
if entry.data.get(CONF_HOST) == user_input[CONF_HOST]:
return self.async_abort(reason="already_configured")
try:
api = await validate_input(self.hass, user_input)
@@ -106,9 +110,10 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if not errors:
await self.async_set_unique_id(api.gateway_id)
await self.async_set_unique_id(
api.smile_hostname or api.gateway_id, raise_on_progress=False
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=api.smile_name, data=user_input)

View File

@@ -20,4 +20,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if entry and import_config.items() != entry.data.items():
self.hass.config_entries.async_update_entry(entry, data=import_config)
return self.async_abort(reason="already_configured")
self._abort_if_unique_id_configured()
return self.async_create_entry(title="RFXTRX", data=import_config)

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/risco",
"requirements": [
"pyrisco==0.3.0"
"pyrisco==0.3.1"
],
"codeowners": [
"@OnFreund"

View File

@@ -0,0 +1,154 @@
"""Support for media browsing."""
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import (
MEDIA_CLASS_APP,
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_APP,
MEDIA_TYPE_APPS,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
)
CONTENT_TYPE_MEDIA_CLASS = {
MEDIA_TYPE_APP: MEDIA_CLASS_APP,
MEDIA_TYPE_APPS: MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL,
MEDIA_TYPE_CHANNELS: MEDIA_CLASS_DIRECTORY,
}
PLAYABLE_MEDIA_TYPES = [
MEDIA_TYPE_APP,
MEDIA_TYPE_CHANNEL,
]
EXPANDABLE_MEDIA_TYPES = [
MEDIA_TYPE_APPS,
MEDIA_TYPE_CHANNELS,
]
def build_item_response(coordinator, payload):
"""Create response payload for the provided media query."""
search_id = payload["search_id"]
search_type = payload["search_type"]
thumbnail = None
title = None
media = None
if search_type == MEDIA_TYPE_APPS:
title = "Apps"
media = [
{"app_id": item.app_id, "title": item.name, "type": MEDIA_TYPE_APP}
for item in coordinator.data.apps
]
elif search_type == MEDIA_TYPE_CHANNELS:
title = "Channels"
media = [
{
"channel_number": item.number,
"title": item.name,
"type": MEDIA_TYPE_CHANNEL,
}
for item in coordinator.data.channels
]
if media is None:
return None
return BrowseMedia(
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id=search_id,
media_content_type=search_type,
title=title,
can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id,
can_expand=True,
children=[item_payload(item, coordinator) for item in media],
thumbnail=thumbnail,
)
def item_payload(item, coordinator):
"""
Create response payload for a single media item.
Used by async_browse_media.
"""
thumbnail = None
if "app_id" in item:
media_content_type = MEDIA_TYPE_APP
media_content_id = item["app_id"]
thumbnail = coordinator.roku.app_icon_url(item["app_id"])
elif "channel_number" in item:
media_content_type = MEDIA_TYPE_CHANNEL
media_content_id = item["channel_number"]
else:
media_content_type = item["type"]
media_content_id = ""
title = item["title"]
can_play = media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id
can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES
return BrowseMedia(
title=title,
media_class=CONTENT_TYPE_MEDIA_CLASS[media_content_type],
media_content_type=media_content_type,
media_content_id=media_content_id,
can_play=can_play,
can_expand=can_expand,
thumbnail=thumbnail,
)
def library_payload(coordinator):
"""
Create response payload to describe contents of a specific library.
Used by async_browse_media.
"""
library_info = BrowseMedia(
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="library",
media_content_type="library",
title="Media Library",
can_play=False,
can_expand=True,
children=[],
)
library = {
MEDIA_TYPE_APPS: "Apps",
MEDIA_TYPE_CHANNELS: "Channels",
}
for item in [{"title": name, "type": type_} for type_, name in library.items()]:
if (
item["type"] == MEDIA_TYPE_CHANNELS
and coordinator.data.info.device_type != "tv"
):
continue
library_info.children.append(
item_payload(
{"title": item["title"], "type": item["type"]},
coordinator,
)
)
if all(
child.media_content_type == MEDIA_TYPE_APPS for child in library_info.children
):
library_info.children_media_class = MEDIA_CLASS_APP
elif all(
child.media_content_type == MEDIA_TYPE_CHANNELS
for child in library_info.children
):
library_info.children_media_class = MEDIA_CLASS_CHANNEL
else:
library_info.children_media_class = MEDIA_CLASS_DIRECTORY
return library_info

View File

@@ -7,19 +7,11 @@ import voluptuous as vol
from homeassistant.components.media_player import (
DEVICE_CLASS_RECEIVER,
DEVICE_CLASS_TV,
BrowseMedia,
MediaPlayerEntity,
)
from homeassistant.components.media_player.const import (
MEDIA_CLASS_APP,
MEDIA_CLASS_APPS,
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_CHANNELS,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_APP,
MEDIA_TYPE_APPS,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
@@ -44,6 +36,7 @@ from homeassistant.const import (
from homeassistant.helpers import entity_platform
from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler
from .browse_media import build_item_response, library_payload
from .const import ATTR_KEYWORD, DOMAIN, SERVICE_SEARCH
_LOGGER = logging.getLogger(__name__)
@@ -80,44 +73,6 @@ async def async_setup_entry(hass, entry, async_add_entities):
)
def browse_media_library(channels: bool = False) -> BrowseMedia:
"""Create response payload to describe contents of a specific library."""
library_info = BrowseMedia(
title="Media Library",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="library",
media_content_type="library",
can_play=False,
can_expand=True,
children=[],
)
library_info.children.append(
BrowseMedia(
title="Apps",
media_class=MEDIA_CLASS_APPS,
media_content_id="apps",
media_content_type=MEDIA_TYPE_APPS,
can_expand=True,
can_play=False,
)
)
if channels:
library_info.children.append(
BrowseMedia(
title="Channels",
media_class=MEDIA_CLASS_CHANNELS,
media_content_id="channels",
media_content_type=MEDIA_TYPE_CHANNELS,
can_expand=True,
can_play=False,
)
)
return library_info
class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
"""Representation of a Roku media player on the network."""
@@ -286,53 +241,13 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
if media_content_type in [None, "library"]:
is_tv = self.coordinator.data.info.device_type == "tv"
return browse_media_library(channels=is_tv)
return library_payload(self.coordinator)
response = None
if media_content_type == MEDIA_TYPE_APPS:
response = BrowseMedia(
title="Apps",
media_class=MEDIA_CLASS_APPS,
media_content_id="apps",
media_content_type=MEDIA_TYPE_APPS,
can_expand=True,
can_play=False,
children=[
BrowseMedia(
title=app.name,
thumbnail=self.coordinator.roku.app_icon_url(app.app_id),
media_class=MEDIA_CLASS_APP,
media_content_id=app.app_id,
media_content_type=MEDIA_TYPE_APP,
can_play=True,
can_expand=False,
)
for app in self.coordinator.data.apps
],
)
if media_content_type == MEDIA_TYPE_CHANNELS:
response = BrowseMedia(
title="Channels",
media_class=MEDIA_CLASS_CHANNELS,
media_content_id="channels",
media_content_type=MEDIA_TYPE_CHANNELS,
can_expand=True,
can_play=False,
children=[
BrowseMedia(
title=channel.name,
media_class=MEDIA_CLASS_CHANNEL,
media_content_id=channel.number,
media_content_type=MEDIA_TYPE_CHANNEL,
can_play=True,
can_expand=False,
)
for channel in self.coordinator.data.channels
],
)
payload = {
"search_type": media_content_type,
"search_id": media_content_id,
}
response = build_item_response(self.coordinator, payload)
if response is None:
raise BrowseError(

View File

@@ -12,6 +12,7 @@ from homeassistant.const import (
CONF_ICON,
CONF_MODE,
CONF_SEQUENCE,
CONF_VARIABLES,
SERVICE_RELOAD,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
@@ -59,6 +60,7 @@ SCRIPT_ENTRY_SCHEMA = make_script_schema(
vol.Optional(CONF_ICON): cv.icon,
vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_DESCRIPTION, default=""): cv.string,
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
vol.Optional(CONF_FIELDS, default={}): {
cv.string: {
vol.Optional(CONF_DESCRIPTION): cv.string,
@@ -75,7 +77,7 @@ CONFIG_SCHEMA = vol.Schema(
SCRIPT_SERVICE_SCHEMA = vol.Schema(dict)
SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema(
{vol.Optional(ATTR_VARIABLES): dict}
{vol.Optional(ATTR_VARIABLES): {str: cv.match_all}}
)
RELOAD_SERVICE_SCHEMA = vol.Schema({})
@@ -263,6 +265,7 @@ class ScriptEntity(ToggleEntity):
max_runs=cfg[CONF_MAX],
max_exceeded=cfg[CONF_MAX_EXCEEDED],
logger=logging.getLogger(f"{__name__}.{object_id}"),
variables=cfg.get(CONF_VARIABLES),
)
self._changed = asyncio.Event()

View File

@@ -42,11 +42,11 @@ async def async_setup_entry_attribute_entities(
if not blocks:
return
counts = Counter([item[0].type for item in blocks])
counts = Counter([item[1] for item in blocks])
async_add_entities(
[
sensor_class(wrapper, block, sensor_id, description, counts[block.type])
sensor_class(wrapper, block, sensor_id, description, counts[sensor_id])
for block, sensor_id, description in blocks
]
)

View File

@@ -3,7 +3,7 @@
"name": "Shelly",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==0.3.0"],
"zeroconf": ["_http._tcp.local."],
"requirements": ["aioshelly==0.3.2"],
"zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
"codeowners": ["@balloob", "@bieniu"]
}

View File

@@ -1,5 +1,5 @@
"""Switch for Shelly."""
from aioshelly import RelayBlock
from aioshelly import Block
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import callback
@@ -13,6 +13,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up switches for device."""
wrapper = hass.data[DOMAIN][config_entry.entry_id]
# In roller mode the relay blocks exist but do not contain required info
if (
wrapper.model in ["SHSW-21", "SHSW-25"]
and wrapper.device.settings["mode"] != "relay"
):
return
relay_blocks = [block for block in wrapper.device.blocks if block.type == "relay"]
if not relay_blocks:
@@ -24,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class RelaySwitch(ShellyBlockEntity, SwitchEntity):
"""Switch that controls a relay block on Shelly devices."""
def __init__(self, wrapper: ShellyDeviceWrapper, block: RelayBlock) -> None:
def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None:
"""Initialize relay switch."""
super().__init__(wrapper, block)
self.control_result = None

View File

@@ -202,22 +202,23 @@ class SlackNotificationService(BaseNotificationService):
self, targets, message, title, blocks, username, icon
):
"""Send a text-only message."""
if self._icon.lower().startswith(("http://", "https://")):
icon_type = "url"
else:
icon_type = "emoji"
message_dict = {
"blocks": blocks,
"link_names": True,
"text": message,
"username": username,
}
if self._icon:
if self._icon.lower().startswith(("http://", "https://")):
icon_type = "url"
else:
icon_type = "emoji"
message_dict[f"icon_{icon_type}"] = icon
tasks = {
target: self._client.chat_postMessage(
**{
"blocks": blocks,
"channel": target,
"link_names": True,
"text": message,
"username": username,
f"icon_{icon_type}": icon,
}
)
target: self._client.chat_postMessage(**message_dict, channel=target)
for target in targets
}

View File

@@ -5,12 +5,13 @@
"documentation": "https://www.home-assistant.io/integrations/smappee",
"dependencies": ["http"],
"requirements": [
"pysmappee==0.2.10"
"pysmappee==0.2.13"
],
"codeowners": [
"@bsmappee"
],
"zeroconf": [
"_ssh._tcp.local."
{"type":"_ssh._tcp.local.", "name":"smappee1*"},
{"type":"_ssh._tcp.local.", "name":"smappee2*"}
]
}

View File

@@ -1,34 +1,35 @@
{
"config": {
"flow_title": "Smappee: {name}",
"step": {
"environment": {
"description": "Set up your Smappee to integrate with Home Assistant.",
"data": {
"environment": "Environment"
}
},
"local": {
"description": "Enter the host to initiate the Smappee local integration",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
},
"zeroconf_confirm": {
"description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?",
"title": "Discovered Smappee device"
},
"pick_implementation": {
"title": "Pick Authentication Method"
}
},
"abort": {
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
"already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.",
"authorize_url_timeout": "Timeout generating authorize url.",
"connection_error": "Failed to connect to Smappee device.",
"missing_configuration": "The component is not configured. Please follow the documentation.",
"invalid_mdns": "Unsupported device for the Smappee integration."
"config": {
"flow_title": "Smappee: {name}",
"step": {
"environment": {
"description": "Set up your Smappee to integrate with Home Assistant.",
"data": {
"environment": "Environment"
}
},
"local": {
"description": "Enter the host to initiate the Smappee local integration",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
},
"zeroconf_confirm": {
"description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?",
"title": "Discovered Smappee device"
},
"pick_implementation": {
"title": "Pick Authentication Method"
}
},
"abort": {
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
"already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.",
"authorize_url_timeout": "Timeout generating authorize url.",
"connection_error": "Failed to connect to Smappee device.",
"missing_configuration": "The component is not configured. Please follow the documentation.",
"invalid_mdns": "Unsupported device for the Smappee integration.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
}
}
}

View File

@@ -6,7 +6,8 @@
"abort": {
"already_setup": "You can only configure one Somfy account.",
"authorize_url_timeout": "Timeout generating authorize url.",
"missing_configuration": "The Somfy component is not configured. Please follow the documentation."
"missing_configuration": "The Somfy component is not configured. Please follow the documentation.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
},
"create_entry": { "default": "Successfully authenticated with Somfy." }
}

View File

@@ -222,6 +222,10 @@ ATTR_STATUS_LIGHT = "status_light"
UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None}
class UnknownMediaType(BrowseError):
"""Unknown media type."""
class SonosData:
"""Storage class for platform global data."""
@@ -1487,7 +1491,20 @@ def build_item_response(media_library, payload):
except IndexError:
title = LIBRARY_TITLES_MAPPING[payload["idstring"]]
media_class = SONOS_TO_MEDIA_CLASSES[MEDIA_TYPES_TO_SONOS[payload["search_type"]]]
try:
media_class = SONOS_TO_MEDIA_CLASSES[
MEDIA_TYPES_TO_SONOS[payload["search_type"]]
]
except KeyError:
_LOGGER.debug("Unknown media type received %s", payload["search_type"])
return None
children = []
for item in media:
try:
children.append(item_payload(item))
except UnknownMediaType:
pass
return BrowseMedia(
title=title,
@@ -1495,7 +1512,7 @@ def build_item_response(media_library, payload):
media_class=media_class,
media_content_id=payload["idstring"],
media_content_type=payload["search_type"],
children=[item_payload(item) for item in media],
children=children,
can_play=can_play(payload["search_type"]),
can_expand=can_expand(payload["search_type"]),
)
@@ -1507,12 +1524,18 @@ def item_payload(item):
Used by async_browse_media.
"""
media_type = get_media_type(item)
try:
media_class = SONOS_TO_MEDIA_CLASSES[media_type]
except KeyError as err:
_LOGGER.debug("Unknown media type received %s", media_type)
raise UnknownMediaType from err
return BrowseMedia(
title=item.title,
thumbnail=getattr(item, "album_art_uri", None),
media_class=SONOS_TO_MEDIA_CLASSES[get_media_type(item)],
media_class=media_class,
media_content_id=get_content_id(item),
media_content_type=SONOS_TO_MEDIA_TYPES[get_media_type(item)],
media_content_type=SONOS_TO_MEDIA_TYPES[media_type],
can_play=can_play(item.item_class),
can_expand=can_expand(item),
)
@@ -1524,6 +1547,20 @@ def library_payload(media_library):
Used by async_browse_media.
"""
if not media_library.browse_by_idstring(
"tracks",
"",
max_items=1,
):
raise BrowseError("Local library not found")
children = []
for item in media_library.browse():
try:
children.append(item_payload(item))
except UnknownMediaType:
pass
return BrowseMedia(
title="Music Library",
media_class=MEDIA_CLASS_DIRECTORY,
@@ -1531,7 +1568,7 @@ def library_payload(media_library):
media_content_type="library",
can_play=False,
can_expand=True,
children=[item_payload(item) for item in media_library.browse()],
children=children,
)

View File

@@ -143,9 +143,12 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator):
self.servers[DEFAULT_SERVER] = {}
for server in sorted(
server_list.values(), key=lambda server: server[0]["country"]
server_list.values(),
key=lambda server: server[0]["country"] + server[0]["sponsor"],
):
self.servers[f"{server[0]['country']} - {server[0]['sponsor']}"] = server[0]
self.servers[
f"{server[0]['country']} - {server[0]['sponsor']} - {server[0]['name']}"
] = server[0]
def update_data(self):
"""Get the latest data from speedtest.net."""

View File

@@ -15,6 +15,7 @@ from homeassistant.components.media_player.const import (
MEDIA_CLASS_ARTIST,
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_EPISODE,
MEDIA_CLASS_GENRE,
MEDIA_CLASS_PLAYLIST,
MEDIA_CLASS_PODCAST,
MEDIA_CLASS_TRACK,
@@ -104,24 +105,57 @@ LIBRARY_MAP = {
}
CONTENT_TYPE_MEDIA_CLASS = {
"current_user_playlists": MEDIA_CLASS_PLAYLIST,
"current_user_followed_artists": MEDIA_CLASS_ARTIST,
"current_user_saved_albums": MEDIA_CLASS_ALBUM,
"current_user_saved_tracks": MEDIA_CLASS_TRACK,
"current_user_saved_shows": MEDIA_CLASS_PODCAST,
"current_user_recently_played": MEDIA_CLASS_TRACK,
"current_user_top_artists": MEDIA_CLASS_ARTIST,
"current_user_top_tracks": MEDIA_CLASS_TRACK,
"featured_playlists": MEDIA_CLASS_PLAYLIST,
"categories": MEDIA_CLASS_DIRECTORY,
"category_playlists": MEDIA_CLASS_PLAYLIST,
"new_releases": MEDIA_CLASS_ALBUM,
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE,
MEDIA_TYPE_SHOW: MEDIA_CLASS_PODCAST,
MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK,
"current_user_playlists": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_PLAYLIST,
},
"current_user_followed_artists": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_ARTIST,
},
"current_user_saved_albums": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_ALBUM,
},
"current_user_saved_tracks": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_TRACK,
},
"current_user_saved_shows": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_PODCAST,
},
"current_user_recently_played": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_TRACK,
},
"current_user_top_artists": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_ARTIST,
},
"current_user_top_tracks": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_TRACK,
},
"featured_playlists": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_PLAYLIST,
},
"categories": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_GENRE},
"category_playlists": {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_PLAYLIST,
},
"new_releases": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ALBUM},
MEDIA_TYPE_PLAYLIST: {
"parent": MEDIA_CLASS_PLAYLIST,
"children": MEDIA_CLASS_TRACK,
},
MEDIA_TYPE_ALBUM: {"parent": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_TRACK},
MEDIA_TYPE_ARTIST: {"parent": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ALBUM},
MEDIA_TYPE_EPISODE: {"parent": MEDIA_CLASS_EPISODE, "children": None},
MEDIA_TYPE_SHOW: {"parent": MEDIA_CLASS_PODCAST, "children": MEDIA_CLASS_EPISODE},
MEDIA_TYPE_TRACK: {"parent": MEDIA_CLASS_TRACK, "children": None},
}
@@ -542,7 +576,8 @@ def build_item_response(spotify, user, payload):
if media_content_type == "categories":
media_item = BrowseMedia(
title=LIBRARY_MAP.get(media_content_id),
media_class=media_class,
media_class=media_class["parent"],
children_media_class=media_class["children"],
media_content_id=media_content_id,
media_content_type=media_content_type,
can_play=False,
@@ -559,6 +594,7 @@ def build_item_response(spotify, user, payload):
BrowseMedia(
title=item.get("name"),
media_class=MEDIA_CLASS_PLAYLIST,
children_media_class=MEDIA_CLASS_TRACK,
media_content_id=item_id,
media_content_type="category_playlists",
thumbnail=fetch_image_url(item, key="icons"),
@@ -566,6 +602,7 @@ def build_item_response(spotify, user, payload):
can_expand=True,
)
)
return media_item
if title is None:
if "name" in media:
@@ -573,9 +610,10 @@ def build_item_response(spotify, user, payload):
else:
title = LIBRARY_MAP.get(payload["media_content_id"])
response = {
params = {
"title": title,
"media_class": media_class,
"media_class": media_class["parent"],
"children_media_class": media_class["children"],
"media_content_id": media_content_id,
"media_content_type": media_content_type,
"can_play": media_content_type in PLAYABLE_MEDIA_TYPES,
@@ -584,16 +622,16 @@ def build_item_response(spotify, user, payload):
}
for item in items:
try:
response["children"].append(item_payload(item))
params["children"].append(item_payload(item))
except (MissingMediaInformation, UnknownMediaType):
continue
if "images" in media:
response["thumbnail"] = fetch_image_url(media)
params["thumbnail"] = fetch_image_url(media)
elif image:
response["thumbnail"] = image
params["thumbnail"] = image
return BrowseMedia(**response)
return BrowseMedia(**params)
def item_payload(item):
@@ -622,17 +660,14 @@ def item_payload(item):
payload = {
"title": item.get("name"),
"media_class": media_class["parent"],
"children_media_class": media_class["children"],
"media_content_id": media_id,
"media_content_type": media_type,
"can_play": media_type in PLAYABLE_MEDIA_TYPES,
"can_expand": can_expand,
}
payload = {
**payload,
"media_class": media_class,
}
if "images" in item:
payload["thumbnail"] = fetch_image_url(item)
elif MEDIA_TYPE_ALBUM in item:
@@ -663,7 +698,9 @@ def library_payload():
{"name": item["name"], "type": item["type"], "uri": item["type"]}
)
)
return BrowseMedia(**library_info)
response = BrowseMedia(**library_info)
response.children_media_class = MEDIA_CLASS_DIRECTORY
return response
def fetch_image_url(item, key="images"):

View File

@@ -10,6 +10,7 @@
"abort": {
"already_setup": "You can only configure one Spotify account.",
"authorize_url_timeout": "Timeout generating authorize url.",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"missing_configuration": "The Spotify integration is not configured. Please follow the documentation.",
"reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication."
},

View File

@@ -122,6 +122,7 @@ class SQLSensor(Entity):
def update(self):
"""Retrieve sensor data from the query."""
data = None
try:
sess = self.sessionmaker()
result = sess.execute(self._query)
@@ -147,7 +148,7 @@ class SQLSensor(Entity):
finally:
sess.close()
if self._template is not None:
if data is not None and self._template is not None:
self._state = self._template.async_render_with_possible_json_value(
data, None
)

View File

@@ -148,7 +148,8 @@ class HlsStreamOutput(StreamOutput):
def container_options(self) -> Callable[[int], dict]:
"""Return Callable which takes a sequence number and returns container options."""
return lambda sequence: {
"movflags": "frag_custom+empty_moov+default_base_moof+skip_sidx+frag_discont",
# Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970
"movflags": "frag_custom+empty_moov+default_base_moof+frag_discont",
"avoid_negative_ts": "make_non_negative",
"fragment_index": str(sequence),
}

View File

@@ -25,7 +25,10 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence):
segment,
mode="w",
format=stream_output.format,
container_options=container_options,
container_options={
"video_track_timescale": str(int(1 / video_stream.time_base)),
**container_options,
},
)
vstream = output.add_stream(template=video_stream)
# Check if audio is requested
@@ -64,11 +67,16 @@ def _stream_worker_internal(hass, stream, quit_event):
video_stream = container.streams.video[0]
except (KeyError, IndexError):
_LOGGER.error("Stream has no video")
container.close()
return
try:
audio_stream = container.streams.audio[0]
except (KeyError, IndexError):
audio_stream = None
# These formats need aac_adtstoasc bitstream filter, but auto_bsf not
# compatible with empty_moov and manual bitstream filters not in PyAV
if container.format.name in {"hls", "mpegts"}:
audio_stream = None
# The presentation timestamps of the first packet in each stream we receive
# Use to adjust before muxing or outputting, but we don't adjust internally
@@ -238,7 +246,7 @@ def _stream_worker_internal(hass, stream, quit_event):
# Update last_dts processed
last_dts[packet.stream] = packet.dts
# mux video packets immediately, save audio packets to be muxed all at once
# mux packets
if packet.stream == video_stream:
mux_video_packet(packet) # mutates packet timestamps
else:

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