Compare commits

..

46 Commits

Author SHA1 Message Date
Franck Nijhof
d520a02b8c Merge pull request #32932 from home-assistant/rc
0.107.0
2020-03-18 15:35:44 +01:00
SukramJ
1e469b39ad Fix flaky tests for HMIPC (#32806) 2020-03-18 14:44:29 +01:00
Franck Nijhof
c2f615839d Bumped version to 0.107.0 2020-03-18 13:31:02 +01:00
Bram Kragten
657bf33e32 Updated frontend to 20200318.0 (#32931) 2020-03-18 13:27:21 +01:00
Paulus Schoutsen
0ca87007fd Bumped version to 0.107.0b8 2020-03-17 17:56:18 -07:00
Paulus Schoutsen
d0d9d853f2 Fix hassio panel load (#32922)
* Fix loading hassio panel

* Remove blacklist
2020-03-17 17:55:57 -07:00
Hans Oischinger
8348878e7e Introduce safe scan_interval for vicare (#32915) 2020-03-17 17:55:56 -07:00
Paulus Schoutsen
b70be5f2f2 Blacklist auto_backup (#32912)
* Blacklist auto_backup

* Mock with a set
2020-03-17 17:55:55 -07:00
Bram Kragten
fddb565e4c Fix input text reload (#32911)
* Fix input text reload

* FIx schema instead
2020-03-17 17:55:54 -07:00
Rami Mosleh
f3e6820042 Fix setting up options due to config data freeze (#32872) 2020-03-17 17:55:54 -07:00
Paulus Schoutsen
ae98f13181 Bumped version to 0.107.0b7 2020-03-17 10:36:59 -07:00
Paulus Schoutsen
ab38e7d98a Bump cast to 4.2.0 (#32906) 2020-03-17 10:35:39 -07:00
brubaked
9797b09d44 Changed Sensor icons to be more emotionally sensitive (#32904)
The existing sensor icons, while descriptive - dead = dead - are perhaps too matter of fact and don't accurately convey the tragedy. I changed emoticon-dead-outline to emoticon-cry-outline, as I think it better conveys the reality of the situation along with the emotions tied to the statistic.
2020-03-17 10:35:39 -07:00
Quentame
4908d4358c Bump iCloud to 0.9.5 (#32901) 2020-03-17 10:35:38 -07:00
Paulus Schoutsen
67d728fc50 Make zone dependency of device tracker an after dep (#32880)
* Make zone dependency of device tracker an after dep

* Fix test
2020-03-17 10:34:34 -07:00
Jason Lachowsky
912409ed0c Corrected minor misspellings (#32857) 2020-03-17 10:32:01 -07:00
Paolo Tuninetto
ac8c889b0f Add default port to samsung tv (#32820)
* Default port for websocket tv

* Update config entry

* move bridge creation

* fix indent

* remove loop
2020-03-17 10:32:00 -07:00
Quentame
67a721d39b Fix iCloud init while pending (#32750)
* Fix iCloud init while pending

Continue if device is pending while setup
Create devices and fetch 15s if pending, otherwise determine interval to fetch.

* Add retried_fetch guard
2020-03-17 10:32:00 -07:00
Paulus Schoutsen
d196fd136d Bumped version to 0.107.0b6 2020-03-16 14:53:13 -07:00
Bram Kragten
4f78674a4c Updated frontend to 20200316.1 (#32878) 2020-03-16 14:53:05 -07:00
Bram Kragten
a7aca10668 Lovelace: storage key based on id instead of url_path (#32873)
* Fix storage key based on url_path

* Fix test
2020-03-16 14:53:04 -07:00
Paulus Schoutsen
03b1c6ddee Remove group as a dependency from entity integrations (#32870)
* remove group dependency

* Update device sun light trigger

* Add zone dep back to device tracker
2020-03-16 14:53:03 -07:00
David F. Mulcahey
661f1b69f2 Bump ZHA quirks to 0.0.37 (#32867) 2020-03-16 14:53:03 -07:00
Bram Kragten
ccb34083fe Add lovelace reload service for yaml resources (#32865)
* Lovelace add reload service for yaml resources

* Clean up imports

* Comments
2020-03-16 14:53:02 -07:00
Bram Kragten
7f6b3c1130 Bumped version to 0.107.0b5 2020-03-16 13:59:27 +01:00
Bram Kragten
f2c3f76b8e Updated frontend to 20200316.0 (#32866) 2020-03-16 13:49:50 +01:00
Pascal Vizeli
b6a3bcf87f Update pyozw 0.1.9 (#32864) 2020-03-16 13:49:49 +01:00
Tom Harris
65423bb62b Bump insteonplm to 0.16.8 (#32847) 2020-03-16 13:49:48 +01:00
Kit Klein
104665d849 Ignore the ignored konnected config entries (#32845)
* ignore the ignored konnected config entries

* key off data instead of source
2020-03-16 13:49:47 +01:00
Alan Tse
fb1ba86b08 Bump teslajsonpy to 0.5.1 (#32827) 2020-03-16 13:49:47 +01:00
David Bonnes
cee72724b6 Ensure unique_ids for all evohome thermostats (#32604)
* initial commit

* small tweak
2020-03-16 13:49:46 +01:00
Paulus Schoutsen
a3d74651a8 Bumped version to 0.107.0b4 2020-03-15 11:56:56 -07:00
Paulus Schoutsen
d88275d6d2 Make sure panel_custom won't crash on invalid data (#32835)
* Make sure panel_custom won't crash on invalid data

* Add a test
2020-03-15 11:52:54 -07:00
SukramJ
42998f898b Add SF transition to HmIP-BSL and remove obsolete code in HMIPC (#32833) 2020-03-15 11:52:53 -07:00
Daniel Høyer Iversen
875671cc2b Add Netatmo Home Coach as model (#32829) 2020-03-15 11:52:53 -07:00
Bram Kragten
3b84b6e6d5 Require a hyphen in lovelace dashboard url (#32816)
* Require a hyphen in lovelace dashboard url

* Keep storage dashboards working

* register during startup again

* Update homeassistant/components/lovelace/dashboard.py

Co-Authored-By: Paulus Schoutsen <balloob@gmail.com>

* Comments

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-03-15 11:52:52 -07:00
Chris Talkington
3b1fb2f416 Remove extra logging from directv init. (#32809) 2020-03-15 11:52:51 -07:00
Chris Talkington
226a0bcaad Fix directv location of unknown error string (#32807)
* Update strings.json

* Update en.json
2020-03-15 11:52:50 -07:00
Greg
57dd45318d Bump eagle_reader API version to v0.2.4 (#32789) 2020-03-15 11:52:50 -07:00
Franck Nijhof
e666485ea9 Fix brightness_pct in light device turn_on action (#32787) 2020-03-15 11:52:49 -07:00
Aidan Timson
b5c8b5b91f Fix onvif error with non ptz cameras (#32783) 2020-03-15 11:52:48 -07:00
David F. Mulcahey
706607f1d2 Fix handling of attribute reports in ZHA sensors and binary sensors (#32776)
* Update sensor tests.

* Update light tests.

* Update binary_sensor tests.

* Update cover tests.

* Update device tracker tests.

* Update fan tests.

* Update lock tests.

* Update switch tests.

* add sensor attr to sensors

* add sensor attr to binary sensors

* cleanup extra var

Co-authored-by: Alexei Chetroi <alexei.chetroi@outlook.com>
2020-03-15 11:52:47 -07:00
Steven Looman
0788bbd629 Add log message on timeout and update less often for upnp devices (#32740)
* Catch asyncio.TimeoutError, show a proper message instead

* Throttle updates to max once per 30s

* Change code owner

* Fix CODEOWNERS + linting

* Warn on connection timeout
2020-03-15 11:52:46 -07:00
Chris Talkington
1b622925a1 Optimize directv client initialization (#32706)
* Optimize directv client initialization.

* Update config_flow.py

* Update media_player.py

* Update media_player.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update test_media_player.py

* Update __init__.py

* Update media_player.py

* Update test_media_player.py

* Update media_player.py

* Update test_media_player.py

* Update config_flow.py

* Update test_config_flow.py

* Update test_config_flow.py

* Update test_config_flow.py

* Update test_config_flow.py

* Update test_config_flow.py

* Update test_config_flow.py

* Update __init__.py

* Update test_config_flow.py

* Update test_config_flow.py

* Update test_media_player.py

* Update test_media_player.py

* Update __init__.py

* Update __init__.py

* Update __init__.py
2020-03-15 11:52:45 -07:00
Slava
86c4fa0fc5 Add brightness state to emulated hue when devices support only color temp and brightness (#31834) 2020-03-15 11:52:45 -07:00
Jc2k
e365dc398b Fix homekit_controller beta connectivity issues (#32810) 2020-03-14 15:43:09 -04:00
93 changed files with 632 additions and 496 deletions

View File

@@ -386,7 +386,7 @@ homeassistant/components/unifiled/* @florisvdk
homeassistant/components/upc_connect/* @pvizeli homeassistant/components/upc_connect/* @pvizeli
homeassistant/components/upcloud/* @scop homeassistant/components/upcloud/* @scop
homeassistant/components/updater/* @home-assistant/core homeassistant/components/updater/* @home-assistant/core
homeassistant/components/upnp/* @robbiet480 homeassistant/components/upnp/* @StevenLooman
homeassistant/components/uptimerobot/* @ludeeus homeassistant/components/uptimerobot/* @ludeeus
homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/usgs_earthquakes_feed/* @exxamalte
homeassistant/components/utility_meter/* @dgomes homeassistant/components/utility_meter/* @dgomes

View File

@@ -3,7 +3,8 @@
"name": "Automation", "name": "Automation",
"documentation": "https://www.home-assistant.io/integrations/automation", "documentation": "https://www.home-assistant.io/integrations/automation",
"requirements": [], "requirements": [],
"dependencies": ["device_automation", "group", "webhook"], "dependencies": [],
"after_dependencies": ["device_automation", "webhook"],
"codeowners": ["@home-assistant/core"], "codeowners": ["@home-assistant/core"],
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

@@ -3,7 +3,7 @@
"name": "Google Cast", "name": "Google Cast",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast", "documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==4.1.1"], "requirements": ["pychromecast==4.2.0"],
"dependencies": [], "dependencies": [],
"after_dependencies": ["cloud"], "after_dependencies": ["cloud"],
"zeroconf": ["_googlecast._tcp.local."], "zeroconf": ["_googlecast._tcp.local."],

View File

@@ -7,9 +7,9 @@ from .const import ATTRIBUTION, OPTION_WORLDWIDE
SENSORS = { SENSORS = {
"confirmed": "mdi:emoticon-neutral-outline", "confirmed": "mdi:emoticon-neutral-outline",
"current": "mdi:emoticon-frown-outline", "current": "mdi:emoticon-sad-outline",
"recovered": "mdi:emoticon-happy-outline", "recovered": "mdi:emoticon-happy-outline",
"deaths": "mdi:emoticon-dead-outline", "deaths": "mdi:emoticon-cry-outline",
} }

View File

@@ -3,7 +3,7 @@
"name": "Cover", "name": "Cover",
"documentation": "https://www.home-assistant.io/integrations/cover", "documentation": "https://www.home-assistant.io/integrations/cover",
"requirements": [], "requirements": [],
"dependencies": ["group"], "dependencies": [],
"codeowners": ["@home-assistant/core"], "codeowners": ["@home-assistant/core"],
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

@@ -3,7 +3,8 @@
"name": "Presence-based Lights", "name": "Presence-based Lights",
"documentation": "https://www.home-assistant.io/integrations/device_sun_light_trigger", "documentation": "https://www.home-assistant.io/integrations/device_sun_light_trigger",
"requirements": [], "requirements": [],
"dependencies": ["device_tracker", "group", "light", "person"], "dependencies": [],
"after_dependencies": ["device_tracker", "group", "light", "person"],
"codeowners": [], "codeowners": [],
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

@@ -3,7 +3,8 @@
"name": "Device Tracker", "name": "Device Tracker",
"documentation": "https://www.home-assistant.io/integrations/device_tracker", "documentation": "https://www.home-assistant.io/integrations/device_tracker",
"requirements": [], "requirements": [],
"dependencies": ["group", "zone"], "dependencies": [],
"after_dependencies": ["zone"],
"codeowners": [], "codeowners": [],
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

@@ -1,11 +1,11 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "DirecTV receiver is already configured" "already_configured": "DirecTV receiver is already configured",
"unknown": "Unexpected error"
}, },
"error": { "error": {
"cannot_connect": "Failed to connect, please try again", "cannot_connect": "Failed to connect, please try again"
"unknown": "Unexpected error"
}, },
"flow_title": "DirecTV: {name}", "flow_title": "DirecTV: {name}",
"step": { "step": {
@@ -23,4 +23,4 @@
}, },
"title": "DirecTV" "title": "DirecTV"
} }
} }

View File

@@ -32,7 +32,7 @@ def get_dtv_data(
hass: HomeAssistant, host: str, port: int = DEFAULT_PORT, client_addr: str = "0" hass: HomeAssistant, host: str, port: int = DEFAULT_PORT, client_addr: str = "0"
) -> dict: ) -> dict:
"""Retrieve a DIRECTV instance, locations list, and version info for the receiver device.""" """Retrieve a DIRECTV instance, locations list, and version info for the receiver device."""
dtv = DIRECTV(host, port, client_addr) dtv = DIRECTV(host, port, client_addr, determine_state=False)
locations = dtv.get_locations() locations = dtv.get_locations()
version_info = dtv.get_version() version_info = dtv.get_version()

View File

@@ -29,8 +29,7 @@ def validate_input(data: Dict) -> Dict:
Data has the keys from DATA_SCHEMA with values provided by the user. Data has the keys from DATA_SCHEMA with values provided by the user.
""" """
# directpy does IO in constructor. dtv = DIRECTV(data["host"], DEFAULT_PORT, determine_state=False)
dtv = DIRECTV(data["host"], DEFAULT_PORT)
version_info = dtv.get_version() version_info = dtv.get_version()
return { return {
@@ -76,8 +75,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN):
return self._show_form(errors) return self._show_form(errors)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = ERROR_UNKNOWN return self.async_abort(reason=ERROR_UNKNOWN)
return self._show_form(errors)
await self.async_set_unique_id(info["receiver_id"]) await self.async_set_unique_id(info["receiver_id"])
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()

View File

@@ -83,22 +83,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
) )
def get_dtv_instance(
host: str, port: int = DEFAULT_PORT, client_addr: str = "0"
) -> DIRECTV:
"""Retrieve a DIRECTV instance for the receiver or client device."""
try:
return DIRECTV(host, port, client_addr)
except RequestException as exception:
_LOGGER.debug(
"Request exception %s trying to retrieve DIRECTV instance for client address %s on device %s",
exception,
client_addr,
host,
)
return None
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistantType, hass: HomeAssistantType,
entry: ConfigEntry, entry: ConfigEntry,
@@ -114,16 +98,15 @@ async def async_setup_entry(
continue continue
if loc["clientAddr"] != "0": if loc["clientAddr"] != "0":
# directpy does IO in constructor. dtv = DIRECTV(
dtv = await hass.async_add_executor_job( entry.data[CONF_HOST],
get_dtv_instance, entry.data[CONF_HOST], DEFAULT_PORT, loc["clientAddr"] DEFAULT_PORT,
loc["clientAddr"],
determine_state=False,
) )
else: else:
dtv = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] dtv = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT]
if not dtv:
continue
entities.append( entities.append(
DirecTvDevice( DirecTvDevice(
str.title(loc["locationName"]), loc["clientAddr"], dtv, version_info, str.title(loc["locationName"]), loc["clientAddr"], dtv, version_info,
@@ -175,15 +158,6 @@ class DirecTvDevice(MediaPlayerDevice):
self._model = MODEL_HOST self._model = MODEL_HOST
self._software_version = version_info["stbSoftwareVersion"] self._software_version = version_info["stbSoftwareVersion"]
if self._is_client:
_LOGGER.debug(
"Created DirecTV media player for client %s on device %s",
self._name,
device,
)
else:
_LOGGER.debug("Created DirecTV media player for device %s", self._name)
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
_LOGGER.debug("%s: Updating status", self.entity_id) _LOGGER.debug("%s: Updating status", self.entity_id)

View File

@@ -16,11 +16,11 @@
} }
}, },
"error": { "error": {
"cannot_connect": "Failed to connect, please try again", "cannot_connect": "Failed to connect, please try again"
"unknown": "Unexpected error"
}, },
"abort": { "abort": {
"already_configured": "DirecTV receiver is already configured" "already_configured": "DirecTV receiver is already configured",
"unknown": "Unexpected error"
} }
} }
} }

View File

@@ -677,7 +677,11 @@ def entity_to_json(config, entity):
retval["type"] = "Color temperature light" retval["type"] = "Color temperature light"
retval["modelid"] = "HASS312" retval["modelid"] = "HASS312"
retval["state"].update( retval["state"].update(
{HUE_API_STATE_COLORMODE: "ct", HUE_API_STATE_CT: state[STATE_COLOR_TEMP]} {
HUE_API_STATE_COLORMODE: "ct",
HUE_API_STATE_CT: state[STATE_COLOR_TEMP],
HUE_API_STATE_BRI: state[STATE_BRIGHTNESS],
}
) )
elif entity_features & ( elif entity_features & (
SUPPORT_BRIGHTNESS SUPPORT_BRIGHTNESS

View File

@@ -149,7 +149,12 @@ class EvoZone(EvoChild, EvoClimateDevice):
"""Initialize a Honeywell TCC Zone.""" """Initialize a Honeywell TCC Zone."""
super().__init__(evo_broker, evo_device) super().__init__(evo_broker, evo_device)
self._unique_id = evo_device.zoneId if evo_device.modelType.startswith("VisionProWifi"):
# this system does not have a distinct ID for the zone
self._unique_id = f"{evo_device.zoneId}z"
else:
self._unique_id = evo_device.zoneId
self._name = evo_device.name self._name = evo_device.name
self._icon = "mdi:radiator" self._icon = "mdi:radiator"

View File

@@ -3,7 +3,7 @@
"name": "Fan", "name": "Fan",
"documentation": "https://www.home-assistant.io/integrations/fan", "documentation": "https://www.home-assistant.io/integrations/fan",
"requirements": [], "requirements": [],
"dependencies": ["group"], "dependencies": [],
"codeowners": [], "codeowners": [],
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

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

View File

@@ -190,16 +190,15 @@ async def async_setup(hass, config):
hass.http.register_view(HassIOView(host, websession)) hass.http.register_view(HassIOView(host, websession))
if "frontend" in hass.config.components: await hass.components.panel_custom.async_register_panel(
await hass.components.panel_custom.async_register_panel( frontend_url_path="hassio",
frontend_url_path="hassio", webcomponent_name="hassio-main",
webcomponent_name="hassio-main", sidebar_title="Supervisor",
sidebar_title="Supervisor", sidebar_icon="hass:home-assistant",
sidebar_icon="hass:home-assistant", js_url="/api/hassio/app/entrypoint.js",
js_url="/api/hassio/app/entrypoint.js", embed_iframe=True,
embed_iframe=True, require_admin=True,
require_admin=True, )
)
await hassio.update_hass_api(config.get("http", {}), refresh_token) await hassio.update_hass_api(config.get("http", {}), refresh_token)

View File

@@ -3,6 +3,7 @@
"name": "Hass.io", "name": "Hass.io",
"documentation": "https://www.home-assistant.io/hassio", "documentation": "https://www.home-assistant.io/hassio",
"requirements": [], "requirements": [],
"dependencies": ["http", "panel_custom"], "dependencies": ["http"],
"after_dependencies": ["panel_custom"],
"codeowners": ["@home-assistant/hass-io"] "codeowners": ["@home-assistant/hass-io"]
} }

View File

@@ -14,7 +14,7 @@
"busy_error": "Device refused to add pairing as it is already pairing with another controller.", "busy_error": "Device refused to add pairing as it is already pairing with another controller.",
"max_peers_error": "Device refused to add pairing as it has no free pairing storage.", "max_peers_error": "Device refused to add pairing as it has no free pairing storage.",
"max_tries_error": "Device refused to add pairing as it has received more than 100 unsuccessful authentication attempts.", "max_tries_error": "Device refused to add pairing as it has received more than 100 unsuccessful authentication attempts.",
"pairing_failed": "An unhandled error occured while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently.", "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently.",
"unable_to_pair": "Unable to pair, please try again.", "unable_to_pair": "Unable to pair, please try again.",
"unknown_error": "Device reported an unknown error. Pairing failed." "unknown_error": "Device reported an unknown error. Pairing failed."
}, },

View File

@@ -3,7 +3,7 @@
"name": "HomeKit Controller", "name": "HomeKit Controller",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller", "documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["aiohomekit[IP]==0.2.29"], "requirements": ["aiohomekit[IP]==0.2.29.1"],
"dependencies": [], "dependencies": [],
"zeroconf": ["_hap._tcp.local."], "zeroconf": ["_hap._tcp.local."],
"codeowners": ["@Jc2k"] "codeowners": ["@Jc2k"]

View File

@@ -154,7 +154,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerDevice):
homekit_state = self.service.value(CharacteristicsTypes.CURRENT_MEDIA_STATE) homekit_state = self.service.value(CharacteristicsTypes.CURRENT_MEDIA_STATE)
if homekit_state is not None: if homekit_state is not None:
return HK_TO_HA_STATE[homekit_state] return HK_TO_HA_STATE.get(homekit_state, STATE_OK)
return STATE_OK return STATE_OK

View File

@@ -25,7 +25,7 @@
"max_peers_error": "Device refused to add pairing as it has no free pairing storage.", "max_peers_error": "Device refused to add pairing as it has no free pairing storage.",
"busy_error": "Device refused to add pairing as it is already pairing with another controller.", "busy_error": "Device refused to add pairing as it is already pairing with another controller.",
"max_tries_error": "Device refused to add pairing as it has received more than 100 unsuccessful authentication attempts.", "max_tries_error": "Device refused to add pairing as it has received more than 100 unsuccessful authentication attempts.",
"pairing_failed": "An unhandled error occured while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently." "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently."
}, },
"abort": { "abort": {
"no_devices": "No unpaired devices could be found", "no_devices": "No unpaired devices could be found",

View File

@@ -137,11 +137,6 @@ class HomematicipHAP:
job = self.hass.async_create_task(self.get_state()) job = self.hass.async_create_task(self.get_state())
job.add_done_callback(self.get_state_finished) job.add_done_callback(self.get_state_finished)
self._accesspoint_connected = True self._accesspoint_connected = True
else:
# Update home with the given json from arg[0],
# without devices and groups.
self.home.update_home_only(args[0])
@callback @callback
def async_create_entity(self, *args, **kwargs) -> None: def async_create_entity(self, *args, **kwargs) -> None:

View File

@@ -20,6 +20,7 @@ from homeassistant.components.light import (
ATTR_TRANSITION, ATTR_TRANSITION,
SUPPORT_BRIGHTNESS, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR, SUPPORT_COLOR,
SUPPORT_TRANSITION,
Light, Light,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@@ -197,7 +198,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light):
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_TRANSITION
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:

View File

@@ -97,6 +97,7 @@ class IcloudAccount:
self._owner_fullname = None self._owner_fullname = None
self._family_members_fullname = {} self._family_members_fullname = {}
self._devices = {} self._devices = {}
self._retried_fetch = False
self.listeners = [] self.listeners = []
@@ -122,10 +123,6 @@ class IcloudAccount:
_LOGGER.error("No iCloud device found") _LOGGER.error("No iCloud device found")
raise ConfigEntryNotReady raise ConfigEntryNotReady
if DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending":
_LOGGER.warning("Pending devices, trying again ...")
raise ConfigEntryNotReady
self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}"
self._family_members_fullname = {} self._family_members_fullname = {}
@@ -157,28 +154,15 @@ class IcloudAccount:
) )
return return
if DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending":
_LOGGER.warning("Pending devices, trying again in 15s")
self._fetch_interval = 0.25
dispatcher_send(self.hass, self.signal_device_update)
track_point_in_utc_time(
self.hass,
self.keep_alive,
utcnow() + timedelta(minutes=self._fetch_interval),
)
return
# Gets devices infos # Gets devices infos
new_device = False new_device = False
for device in api_devices: for device in api_devices:
status = device.status(DEVICE_STATUS_SET) status = device.status(DEVICE_STATUS_SET)
device_id = status[DEVICE_ID] device_id = status[DEVICE_ID]
device_name = status[DEVICE_NAME] device_name = status[DEVICE_NAME]
device_status = DEVICE_STATUS_CODES.get(status[DEVICE_STATUS], "error")
if ( if (
device_status == "pending" status[DEVICE_BATTERY_STATUS] == "Unknown"
or status[DEVICE_BATTERY_STATUS] == "Unknown"
or status.get(DEVICE_BATTERY_LEVEL) is None or status.get(DEVICE_BATTERY_LEVEL) is None
): ):
continue continue
@@ -198,7 +182,16 @@ class IcloudAccount:
self._devices[device_id].update(status) self._devices[device_id].update(status)
new_device = True new_device = True
self._fetch_interval = self._determine_interval() if (
DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending"
and not self._retried_fetch
):
_LOGGER.warning("Pending devices, trying again in 15s")
self._fetch_interval = 0.25
self._retried_fetch = True
else:
self._fetch_interval = self._determine_interval()
self._retried_fetch = False
dispatcher_send(self.hass, self.signal_device_update) dispatcher_send(self.hass, self.signal_device_update)
if new_device: if new_device:

View File

@@ -3,7 +3,7 @@
"name": "Apple iCloud", "name": "Apple iCloud",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/icloud", "documentation": "https://www.home-assistant.io/integrations/icloud",
"requirements": ["pyicloud==0.9.4"], "requirements": ["pyicloud==0.9.5"],
"dependencies": [], "dependencies": [],
"codeowners": ["@Quentame"] "codeowners": ["@Quentame"]
} }

View File

@@ -88,24 +88,22 @@ def _cv_input_text(cfg):
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: cv.schema_with_slug_keys( DOMAIN: cv.schema_with_slug_keys(
vol.Any( vol.All(
vol.All( lambda value: value or {},
{ {
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int),
vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int),
vol.Optional(CONF_INITIAL, ""): cv.string, vol.Optional(CONF_INITIAL, ""): cv.string,
vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_PATTERN): cv.string, vol.Optional(CONF_PATTERN): cv.string,
vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In( vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In(
[MODE_TEXT, MODE_PASSWORD] [MODE_TEXT, MODE_PASSWORD]
), ),
}, },
_cv_input_text, _cv_input_text,
), ),
None,
)
) )
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
@@ -203,13 +201,6 @@ class InputText(RestoreEntity):
@classmethod @classmethod
def from_yaml(cls, config: typing.Dict) -> "InputText": def from_yaml(cls, config: typing.Dict) -> "InputText":
"""Return entity instance initialized from yaml storage.""" """Return entity instance initialized from yaml storage."""
# set defaults for empty config
config = {
CONF_MAX: CONF_MAX_VALUE,
CONF_MIN: CONF_MIN_VALUE,
CONF_MODE: MODE_TEXT,
**config,
}
input_text = cls(config) input_text = cls(config)
input_text.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_text.entity_id = f"{DOMAIN}.{config[CONF_ID]}"
input_text.editable = False input_text.editable = False

View File

@@ -2,7 +2,7 @@
"domain": "insteon", "domain": "insteon",
"name": "Insteon", "name": "Insteon",
"documentation": "https://www.home-assistant.io/integrations/insteon", "documentation": "https://www.home-assistant.io/integrations/insteon",
"requirements": ["insteonplm==0.16.7"], "requirements": ["insteonplm==0.16.8"],
"dependencies": [], "dependencies": [],
"codeowners": [] "codeowners": []
} }

View File

@@ -306,6 +306,7 @@ class KonnectedView(HomeAssistantView):
[ [
entry.data[CONF_ACCESS_TOKEN] entry.data[CONF_ACCESS_TOKEN]
for entry in hass.config_entries.async_entries(DOMAIN) for entry in hass.config_entries.async_entries(DOMAIN)
if entry.data.get(CONF_ACCESS_TOKEN)
] ]
) )
if auth is None or not next( if auth is None or not next(

View File

@@ -15,7 +15,7 @@ from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers import config_validation as cv, entity_registry
from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRIGHTNESS from . import ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRIGHTNESS
TYPE_BRIGHTNESS_INCREASE = "brightness_increase" TYPE_BRIGHTNESS_INCREASE = "brightness_increase"
TYPE_BRIGHTNESS_DECREASE = "brightness_decrease" TYPE_BRIGHTNESS_DECREASE = "brightness_decrease"
@@ -28,7 +28,7 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
toggle_entity.DEVICE_ACTION_TYPES toggle_entity.DEVICE_ACTION_TYPES
+ [TYPE_BRIGHTNESS_INCREASE, TYPE_BRIGHTNESS_DECREASE] + [TYPE_BRIGHTNESS_INCREASE, TYPE_BRIGHTNESS_DECREASE]
), ),
vol.Optional(ATTR_BRIGHTNESS): vol.All( vol.Optional(ATTR_BRIGHTNESS_PCT): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100) vol.Coerce(int), vol.Range(min=0, max=100)
), ),
} }
@@ -57,8 +57,8 @@ async def async_call_action_from_config(
data[ATTR_BRIGHTNESS_STEP_PCT] = 10 data[ATTR_BRIGHTNESS_STEP_PCT] = 10
elif config[CONF_TYPE] == TYPE_BRIGHTNESS_DECREASE: elif config[CONF_TYPE] == TYPE_BRIGHTNESS_DECREASE:
data[ATTR_BRIGHTNESS_STEP_PCT] = -10 data[ATTR_BRIGHTNESS_STEP_PCT] = -10
elif ATTR_BRIGHTNESS in config: elif ATTR_BRIGHTNESS_PCT in config:
data[ATTR_BRIGHTNESS] = config[ATTR_BRIGHTNESS] data[ATTR_BRIGHTNESS_PCT] = config[ATTR_BRIGHTNESS_PCT]
await hass.services.async_call( await hass.services.async_call(
DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=context DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=context
@@ -125,7 +125,7 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di
return { return {
"extra_fields": vol.Schema( "extra_fields": vol.Schema(
{ {
vol.Optional(ATTR_BRIGHTNESS): vol.All( vol.Optional(ATTR_BRIGHTNESS_PCT): vol.All(
vol.Coerce(int), vol.Range(min=0, max=100) vol.Coerce(int), vol.Range(min=0, max=100)
) )
} }

View File

@@ -3,7 +3,7 @@
"name": "Light", "name": "Light",
"documentation": "https://www.home-assistant.io/integrations/light", "documentation": "https://www.home-assistant.io/integrations/light",
"requirements": [], "requirements": [],
"dependencies": ["group"], "dependencies": [],
"codeowners": [], "codeowners": [],
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

@@ -3,7 +3,7 @@
"name": "Lock", "name": "Lock",
"documentation": "https://www.home-assistant.io/integrations/lock", "documentation": "https://www.home-assistant.io/integrations/lock",
"requirements": [], "requirements": [],
"dependencies": ["group"], "dependencies": [],
"codeowners": [], "codeowners": [],
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

@@ -4,10 +4,14 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components import frontend from homeassistant.components import frontend
from homeassistant.const import CONF_FILENAME, EVENT_HOMEASSISTANT_START from homeassistant.config import async_hass_config_yaml, async_process_component_config
from homeassistant.const import CONF_FILENAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import collection, config_validation as cv from homeassistant.helpers import collection, config_validation as cv
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType
from homeassistant.loader import async_get_integration
from homeassistant.util import sanitize_filename from homeassistant.util import sanitize_filename
from . import dashboard, resources, websocket from . import dashboard, resources, websocket
@@ -25,8 +29,10 @@ from .const import (
MODE_STORAGE, MODE_STORAGE,
MODE_YAML, MODE_YAML,
RESOURCE_CREATE_FIELDS, RESOURCE_CREATE_FIELDS,
RESOURCE_RELOAD_SERVICE_SCHEMA,
RESOURCE_SCHEMA, RESOURCE_SCHEMA,
RESOURCE_UPDATE_FIELDS, RESOURCE_UPDATE_FIELDS,
SERVICE_RELOAD_RESOURCES,
STORAGE_DASHBOARD_CREATE_FIELDS, STORAGE_DASHBOARD_CREATE_FIELDS,
STORAGE_DASHBOARD_UPDATE_FIELDS, STORAGE_DASHBOARD_UPDATE_FIELDS,
url_slug, url_slug,
@@ -62,29 +68,41 @@ CONFIG_SCHEMA = vol.Schema(
) )
async def async_setup(hass, config): async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up the Lovelace commands.""" """Set up the Lovelace commands."""
mode = config[DOMAIN][CONF_MODE] mode = config[DOMAIN][CONF_MODE]
yaml_resources = config[DOMAIN].get(CONF_RESOURCES) yaml_resources = config[DOMAIN].get(CONF_RESOURCES)
frontend.async_register_built_in_panel(hass, DOMAIN, config={"mode": mode}) frontend.async_register_built_in_panel(hass, DOMAIN, config={"mode": mode})
async def reload_resources_service_handler(service_call: ServiceCallType) -> None:
"""Reload yaml resources."""
try:
conf = await async_hass_config_yaml(hass)
except HomeAssistantError as err:
_LOGGER.error(err)
return
integration = await async_get_integration(hass, DOMAIN)
config = await async_process_component_config(hass, conf, integration)
resource_collection = await create_yaml_resource_col(
hass, config[DOMAIN].get(CONF_RESOURCES)
)
hass.data[DOMAIN]["resources"] = resource_collection
if mode == MODE_YAML: if mode == MODE_YAML:
default_config = dashboard.LovelaceYAML(hass, None, None) default_config = dashboard.LovelaceYAML(hass, None, None)
resource_collection = await create_yaml_resource_col(hass, yaml_resources)
if yaml_resources is None: async_register_admin_service(
try: hass,
ll_conf = await default_config.async_load(False) DOMAIN,
except HomeAssistantError: SERVICE_RELOAD_RESOURCES,
pass reload_resources_service_handler,
else: schema=RESOURCE_RELOAD_SERVICE_SCHEMA,
if CONF_RESOURCES in ll_conf: )
_LOGGER.warning(
"Resources need to be specified in your configuration.yaml. Please see the docs."
)
yaml_resources = ll_conf[CONF_RESOURCES]
resource_collection = resources.ResourceYAMLCollection(yaml_resources or [])
else: else:
default_config = dashboard.LovelaceStorage(hass, None) default_config = dashboard.LovelaceStorage(hass, None)
@@ -143,6 +161,7 @@ async def async_setup(hass, config):
return return
if change_type == collection.CHANGE_ADDED: if change_type == collection.CHANGE_ADDED:
existing = hass.data[DOMAIN]["dashboards"].get(url_path) existing = hass.data[DOMAIN]["dashboards"].get(url_path)
if existing: if existing:
@@ -167,38 +186,52 @@ async def async_setup(hass, config):
except ValueError: except ValueError:
_LOGGER.warning("Failed to %s panel %s from storage", change_type, url_path) _LOGGER.warning("Failed to %s panel %s from storage", change_type, url_path)
async def async_setup_dashboards(event): # Process YAML dashboards
"""Register dashboards on startup.""" for url_path, dashboard_conf in hass.data[DOMAIN]["yaml_dashboards"].items():
# Process YAML dashboards # For now always mode=yaml
for url_path, dashboard_conf in hass.data[DOMAIN]["yaml_dashboards"].items(): config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf)
# For now always mode=yaml hass.data[DOMAIN]["dashboards"][url_path] = config
config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf)
hass.data[DOMAIN]["dashboards"][url_path] = config
try: try:
_register_panel(hass, url_path, MODE_YAML, dashboard_conf, False) _register_panel(hass, url_path, MODE_YAML, dashboard_conf, False)
except ValueError: except ValueError:
_LOGGER.warning("Panel url path %s is not unique", url_path) _LOGGER.warning("Panel url path %s is not unique", url_path)
# Process storage dashboards # Process storage dashboards
dashboards_collection = dashboard.DashboardsCollection(hass) dashboards_collection = dashboard.DashboardsCollection(hass)
dashboards_collection.async_add_listener(storage_dashboard_changed) dashboards_collection.async_add_listener(storage_dashboard_changed)
await dashboards_collection.async_load() await dashboards_collection.async_load()
collection.StorageCollectionWebsocket( collection.StorageCollectionWebsocket(
dashboards_collection, dashboards_collection,
"lovelace/dashboards", "lovelace/dashboards",
"dashboard", "dashboard",
STORAGE_DASHBOARD_CREATE_FIELDS, STORAGE_DASHBOARD_CREATE_FIELDS,
STORAGE_DASHBOARD_UPDATE_FIELDS, STORAGE_DASHBOARD_UPDATE_FIELDS,
).async_setup(hass, create_list=False) ).async_setup(hass, create_list=False)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_setup_dashboards)
return True return True
async def create_yaml_resource_col(hass, yaml_resources):
"""Create yaml resources collection."""
if yaml_resources is None:
default_config = dashboard.LovelaceYAML(hass, None, None)
try:
ll_conf = await default_config.async_load(False)
except HomeAssistantError:
pass
else:
if CONF_RESOURCES in ll_conf:
_LOGGER.warning(
"Resources need to be specified in your configuration.yaml. Please see the docs."
)
yaml_resources = ll_conf[CONF_RESOURCES]
return resources.ResourceYAMLCollection(yaml_resources or [])
async def system_health_info(hass): async def system_health_info(hass):
"""Get info for the info page.""" """Get info for the info page."""
return await hass.data[DOMAIN]["dashboards"][None].async_get_info() return await hass.data[DOMAIN]["dashboards"][None].async_get_info()

View File

@@ -41,6 +41,9 @@ RESOURCE_UPDATE_FIELDS = {
vol.Optional(CONF_URL): cv.string, vol.Optional(CONF_URL): cv.string,
} }
SERVICE_RELOAD_RESOURCES = "reload_resources"
RESOURCE_RELOAD_SERVICE_SCHEMA = vol.Schema({})
CONF_TITLE = "title" CONF_TITLE = "title"
CONF_REQUIRE_ADMIN = "require_admin" CONF_REQUIRE_ADMIN = "require_admin"
CONF_SHOW_IN_SIDEBAR = "show_in_sidebar" CONF_SHOW_IN_SIDEBAR = "show_in_sidebar"
@@ -76,6 +79,8 @@ def url_slug(value: Any) -> str:
"""Validate value is a valid url slug.""" """Validate value is a valid url slug."""
if value is None: if value is None:
raise vol.Invalid("Slug should not be None") raise vol.Invalid("Slug should not be None")
if "-" not in value:
raise vol.Invalid("Url path needs to contain a hyphen (-)")
str_value = str(value) str_value = str(value)
slg = slugify(str_value, separator="-") slg = slugify(str_value, separator="-")
if str_value == slg: if str_value == slg:

View File

@@ -3,6 +3,7 @@ from abc import ABC, abstractmethod
import logging import logging
import os import os
import time import time
from typing import Optional, cast
import voluptuous as vol import voluptuous as vol
@@ -87,7 +88,7 @@ class LovelaceStorage(LovelaceConfig):
storage_key = CONFIG_STORAGE_KEY_DEFAULT storage_key = CONFIG_STORAGE_KEY_DEFAULT
else: else:
url_path = config[CONF_URL_PATH] url_path = config[CONF_URL_PATH]
storage_key = CONFIG_STORAGE_KEY.format(url_path) storage_key = CONFIG_STORAGE_KEY.format(config["id"])
super().__init__(hass, url_path, config) super().__init__(hass, url_path, config)
@@ -230,8 +231,30 @@ class DashboardsCollection(collection.StorageCollection):
_LOGGER, _LOGGER,
) )
async def _async_load_data(self) -> Optional[dict]:
"""Load the data."""
data = await self.store.async_load()
if data is None:
return cast(Optional[dict], data)
updated = False
for item in data["items"] or []:
if "-" not in item[CONF_URL_PATH]:
updated = True
item[CONF_URL_PATH] = f"lovelace-{item[CONF_URL_PATH]}"
if updated:
await self.store.async_save(data)
return cast(Optional[dict], data)
async def _process_create_data(self, data: dict) -> dict: async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid.""" """Validate the config is valid."""
if "-" not in data[CONF_URL_PATH]:
raise vol.Invalid("Url path needs to contain a hyphen (-)")
if data[CONF_URL_PATH] in self.hass.data[DATA_PANELS]: if data[CONF_URL_PATH] in self.hass.data[DATA_PANELS]:
raise vol.Invalid("Panel url path needs to be unique") raise vol.Invalid("Panel url path needs to be unique")

View File

@@ -0,0 +1,4 @@
# Describes the format for available lovelace services
reload_resources:
description: Reload Lovelace resources from yaml configuration.

View File

@@ -332,16 +332,17 @@ class MikrotikHub:
async def async_add_options(self): async def async_add_options(self):
"""Populate default options for Mikrotik.""" """Populate default options for Mikrotik."""
if not self.config_entry.options: if not self.config_entry.options:
data = dict(self.config_entry.data)
options = { options = {
CONF_ARP_PING: self.config_entry.data.pop(CONF_ARP_PING, False), CONF_ARP_PING: data.pop(CONF_ARP_PING, False),
CONF_FORCE_DHCP: self.config_entry.data.pop(CONF_FORCE_DHCP, False), CONF_FORCE_DHCP: data.pop(CONF_FORCE_DHCP, False),
CONF_DETECTION_TIME: self.config_entry.data.pop( CONF_DETECTION_TIME: data.pop(
CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME
), ),
} }
self.hass.config_entries.async_update_entry( self.hass.config_entries.async_update_entry(
self.config_entry, options=options self.config_entry, data=data, options=options
) )
async def request_update(self): async def request_update(self):

View File

@@ -19,6 +19,7 @@ MODELS = {
"NAModule4": "Smart Additional Indoor module", "NAModule4": "Smart Additional Indoor module",
"NAModule3": "Smart Rain Gauge", "NAModule3": "Smart Rain Gauge",
"NAModule2": "Smart Anemometer", "NAModule2": "Smart Anemometer",
"NHC": "Home Coach",
} }
AUTH = "netatmo_auth" AUTH = "netatmo_auth"

View File

@@ -375,7 +375,7 @@ class ONVIFHassCamera(Camera):
def setup_ptz(self): def setup_ptz(self):
"""Set up PTZ if available.""" """Set up PTZ if available."""
_LOGGER.debug("Setting up the ONVIF PTZ service") _LOGGER.debug("Setting up the ONVIF PTZ service")
if self._camera.get_service("ptz") is None: if self._camera.get_service("ptz", create=False) is None:
_LOGGER.debug("PTZ is not available") _LOGGER.debug("PTZ is not available")
else: else:
self._ptz_service = self._camera.create_ptz_service() self._ptz_service = self._camera.create_ptz_service()

View File

@@ -146,8 +146,6 @@ async def async_setup(hass, config):
if DOMAIN not in config: if DOMAIN not in config:
return True return True
success = False
for panel in config[DOMAIN]: for panel in config[DOMAIN]:
name = panel[CONF_COMPONENT_NAME] name = panel[CONF_COMPONENT_NAME]
@@ -182,8 +180,13 @@ async def async_setup(hass, config):
hass.http.register_static_path(url, panel_path) hass.http.register_static_path(url, panel_path)
kwargs["html_url"] = url kwargs["html_url"] = url
await async_register_panel(hass, **kwargs) try:
await async_register_panel(hass, **kwargs)
except ValueError as err:
_LOGGER.error(
"Unable to register panel %s: %s",
panel.get(CONF_SIDEBAR_TITLE, name),
err,
)
success = True return True
return success

View File

@@ -3,7 +3,7 @@
"name": "Plant Monitor", "name": "Plant Monitor",
"documentation": "https://www.home-assistant.io/integrations/plant", "documentation": "https://www.home-assistant.io/integrations/plant",
"requirements": [], "requirements": [],
"dependencies": ["group", "zone"], "dependencies": [],
"after_dependencies": ["recorder"], "after_dependencies": ["recorder"],
"codeowners": ["@ChristianKuehnel"], "codeowners": ["@ChristianKuehnel"],
"quality_scale": "internal" "quality_scale": "internal"

View File

@@ -3,7 +3,7 @@
"name": "Rainforest Eagle-200", "name": "Rainforest Eagle-200",
"documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle",
"requirements": [ "requirements": [
"eagle200_reader==0.2.1", "eagle200_reader==0.2.4",
"uEagle==0.0.1" "uEagle==0.0.1"
], ],
"dependencies": [], "dependencies": [],

View File

@@ -3,6 +3,6 @@
"name": "Remote", "name": "Remote",
"documentation": "https://www.home-assistant.io/integrations/remote", "documentation": "https://www.home-assistant.io/integrations/remote",
"requirements": [], "requirements": [],
"dependencies": ["group"], "dependencies": [],
"codeowners": [] "codeowners": []
} }

View File

@@ -46,6 +46,7 @@ class SamsungTVBridge(ABC):
self.method = method self.method = method
self.host = host self.host = host
self.token = None self.token = None
self.default_port = None
self._remote = None self._remote = None
self._callback = None self._callback = None
@@ -191,6 +192,7 @@ class SamsungTVWSBridge(SamsungTVBridge):
"""Initialize Bridge.""" """Initialize Bridge."""
super().__init__(method, host, port) super().__init__(method, host, port)
self.token = token self.token = token
self.default_port = 8001
def try_connect(self): def try_connect(self):
"""Try to connect to the Websocket TV.""" """Try to connect to the Websocket TV."""

View File

@@ -71,13 +71,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
): ):
turn_on_action = hass.data[DOMAIN][ip_address][CONF_ON_ACTION] turn_on_action = hass.data[DOMAIN][ip_address][CONF_ON_ACTION]
on_script = Script(hass, turn_on_action) on_script = Script(hass, turn_on_action)
async_add_entities([SamsungTVDevice(config_entry, on_script)])
# Initialize bridge
data = config_entry.data.copy()
bridge = SamsungTVBridge.get_bridge(
data[CONF_METHOD], data[CONF_HOST], data[CONF_PORT], data.get(CONF_TOKEN),
)
if bridge.port is None and bridge.default_port is not None:
# For backward compat, set default port for websocket tv
data[CONF_PORT] = bridge.default_port
hass.config_entries.async_update_entry(config_entry, data=data)
bridge = SamsungTVBridge.get_bridge(
data[CONF_METHOD], data[CONF_HOST], data[CONF_PORT], data.get(CONF_TOKEN),
)
async_add_entities([SamsungTVDevice(bridge, config_entry, on_script)])
class SamsungTVDevice(MediaPlayerDevice): class SamsungTVDevice(MediaPlayerDevice):
"""Representation of a Samsung TV.""" """Representation of a Samsung TV."""
def __init__(self, config_entry, on_script): def __init__(self, bridge, config_entry, on_script):
"""Initialize the Samsung device.""" """Initialize the Samsung device."""
self._config_entry = config_entry self._config_entry = config_entry
self._manufacturer = config_entry.data.get(CONF_MANUFACTURER) self._manufacturer = config_entry.data.get(CONF_MANUFACTURER)
@@ -93,13 +107,7 @@ class SamsungTVDevice(MediaPlayerDevice):
# Mark the end of a shutdown command (need to wait 15 seconds before # Mark the end of a shutdown command (need to wait 15 seconds before
# sending the next command to avoid turning the TV back ON). # sending the next command to avoid turning the TV back ON).
self._end_of_power_off = None self._end_of_power_off = None
# Initialize bridge self._bridge = bridge
self._bridge = SamsungTVBridge.get_bridge(
config_entry.data[CONF_METHOD],
config_entry.data[CONF_HOST],
config_entry.data[CONF_PORT],
config_entry.data.get(CONF_TOKEN),
)
self._bridge.register_reauth_callback(self.access_denied) self._bridge.register_reauth_callback(self.access_denied)
def access_denied(self): def access_denied(self):

View File

@@ -3,7 +3,7 @@
"name": "Scripts", "name": "Scripts",
"documentation": "https://www.home-assistant.io/integrations/script", "documentation": "https://www.home-assistant.io/integrations/script",
"requirements": [], "requirements": [],
"dependencies": ["group"], "dependencies": [],
"codeowners": ["@home-assistant/core"], "codeowners": ["@home-assistant/core"],
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

@@ -3,7 +3,7 @@
"name": "Switch", "name": "Switch",
"documentation": "https://www.home-assistant.io/integrations/switch", "documentation": "https://www.home-assistant.io/integrations/switch",
"requirements": [], "requirements": [],
"dependencies": ["group"], "dependencies": [],
"codeowners": [], "codeowners": [],
"quality_scale": "internal" "quality_scale": "internal"
} }

View File

@@ -91,7 +91,7 @@ class LogEntry:
def __init__(self, record, stack, source): def __init__(self, record, stack, source):
"""Initialize a log entry.""" """Initialize a log entry."""
self.first_occured = self.timestamp = record.created self.first_occurred = self.timestamp = record.created
self.name = record.name self.name = record.name
self.level = record.levelname self.level = record.levelname
self.message = deque([record.getMessage()], maxlen=5) self.message = deque([record.getMessage()], maxlen=5)
@@ -117,7 +117,7 @@ class LogEntry:
"timestamp": self.timestamp, "timestamp": self.timestamp,
"exception": self.exception, "exception": self.exception,
"count": self.count, "count": self.count,
"first_occured": self.first_occured, "first_occurred": self.first_occurred,
} }

View File

@@ -4,7 +4,7 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/tesla", "documentation": "https://www.home-assistant.io/integrations/tesla",
"requirements": [ "requirements": [
"teslajsonpy==0.4.0" "teslajsonpy==0.5.1"
], ],
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [

View File

@@ -5,7 +5,7 @@
"client_secret": "The client secret from the configuration is invalid.", "client_secret": "The client secret from the configuration is invalid.",
"no_agreements": "This account has no Toon displays.", "no_agreements": "This account has no Toon displays.",
"no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/).", "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/).",
"unknown_auth_fail": "Unexpected error occured, while authenticating." "unknown_auth_fail": "Unexpected error occurred, while authenticating."
}, },
"error": { "error": {
"credentials": "The provided credentials are invalid.", "credentials": "The provided credentials are invalid.",

View File

@@ -26,7 +26,7 @@
"abort": { "abort": {
"client_id": "The client ID from the configuration is invalid.", "client_id": "The client ID from the configuration is invalid.",
"client_secret": "The client secret from the configuration is invalid.", "client_secret": "The client secret from the configuration is invalid.",
"unknown_auth_fail": "Unexpected error occured, while authenticating.", "unknown_auth_fail": "Unexpected error occurred, while authenticating.",
"no_agreements": "This account has no Toon displays.", "no_agreements": "This account has no Toon displays.",
"no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/)." "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/)."
} }

View File

@@ -142,16 +142,28 @@ class Device:
async def async_get_total_bytes_received(self): async def async_get_total_bytes_received(self):
"""Get total bytes received.""" """Get total bytes received."""
return await self._igd_device.async_get_total_bytes_received() try:
return await self._igd_device.async_get_total_bytes_received()
except asyncio.TimeoutError:
_LOGGER.warning("Timeout during get_total_bytes_received")
async def async_get_total_bytes_sent(self): async def async_get_total_bytes_sent(self):
"""Get total bytes sent.""" """Get total bytes sent."""
return await self._igd_device.async_get_total_bytes_sent() try:
return await self._igd_device.async_get_total_bytes_sent()
except asyncio.TimeoutError:
_LOGGER.warning("Timeout during get_total_bytes_sent")
async def async_get_total_packets_received(self): async def async_get_total_packets_received(self):
"""Get total packets received.""" """Get total packets received."""
return await self._igd_device.async_get_total_packets_received() try:
return await self._igd_device.async_get_total_packets_received()
except asyncio.TimeoutError:
_LOGGER.warning("Timeout during get_total_packets_received")
async def async_get_total_packets_sent(self): async def async_get_total_packets_sent(self):
"""Get total packets sent.""" """Get total packets sent."""
return await self._igd_device.async_get_total_packets_sent() try:
return await self._igd_device.async_get_total_packets_sent()
except asyncio.TimeoutError:
_LOGGER.warning("Timeout during get_total_packets_sent")

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/upnp", "documentation": "https://www.home-assistant.io/integrations/upnp",
"requirements": ["async-upnp-client==0.14.12"], "requirements": ["async-upnp-client==0.14.12"],
"dependencies": [], "dependencies": [],
"codeowners": ["@robbiet480"] "codeowners": ["@StevenLooman"]
} }

View File

@@ -1,4 +1,5 @@
"""Support for UPnP/IGD Sensors.""" """Support for UPnP/IGD Sensors."""
from datetime import timedelta
import logging import logging
from homeassistant.const import DATA_BYTES, DATA_KIBIBYTES, TIME_SECONDS from homeassistant.const import DATA_BYTES, DATA_KIBIBYTES, TIME_SECONDS
@@ -7,6 +8,7 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .const import DOMAIN as DOMAIN_UPNP, SIGNAL_REMOVE_SENSOR from .const import DOMAIN as DOMAIN_UPNP, SIGNAL_REMOVE_SENSOR
@@ -29,6 +31,8 @@ IN = "received"
OUT = "sent" OUT = "sent"
KIBIBYTE = 1024 KIBIBYTE = 1024
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
async def async_setup_platform( async def async_setup_platform(
hass: HomeAssistantType, config, async_add_entities, discovery_info=None hass: HomeAssistantType, config, async_add_entities, discovery_info=None
@@ -142,6 +146,7 @@ class RawUPnPIGDSensor(UpnpSensor):
"""Return the unit of measurement of this entity, if any.""" """Return the unit of measurement of this entity, if any."""
return self._type["unit"] return self._type["unit"]
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self): async def async_update(self):
"""Get the latest information from the IGD.""" """Get the latest information from the IGD."""
if self._type_name == BYTES_RECEIVED: if self._type_name == BYTES_RECEIVED:

View File

@@ -3,6 +3,6 @@
"name": "Vacuum", "name": "Vacuum",
"documentation": "https://www.home-assistant.io/integrations/vacuum", "documentation": "https://www.home-assistant.io/integrations/vacuum",
"requirements": [], "requirements": [],
"dependencies": ["group"], "dependencies": [],
"codeowners": [] "codeowners": []
} }

View File

@@ -1,4 +1,5 @@
"""Viessmann ViCare climate device.""" """Viessmann ViCare climate device."""
from datetime import timedelta
import logging import logging
import requests import requests
@@ -79,6 +80,9 @@ HA_TO_VICARE_PRESET_HEATING = {
PYVICARE_ERROR = "error" PYVICARE_ERROR = "error"
# Scan interval of 15 minutes seems to be safe to not hit the ViCare server rate limit
SCAN_INTERVAL = timedelta(seconds=900)
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create the ViCare climate devices.""" """Create the ViCare climate devices."""

View File

@@ -1,4 +1,5 @@
"""Viessmann ViCare water_heater device.""" """Viessmann ViCare water_heater device."""
from datetime import timedelta
import logging import logging
import requests import requests
@@ -42,6 +43,9 @@ HA_TO_VICARE_HVAC_DHW = {
PYVICARE_ERROR = "error" PYVICARE_ERROR = "error"
# Scan interval of 15 minutes seems to be safe to not hit the ViCare server rate limit
SCAN_INTERVAL = timedelta(seconds=900)
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create the ViCare water_heater devices.""" """Create the ViCare water_heater devices."""

View File

@@ -64,6 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class BinarySensor(ZhaEntity, BinarySensorDevice): class BinarySensor(ZhaEntity, BinarySensorDevice):
"""ZHA BinarySensor.""" """ZHA BinarySensor."""
SENSOR_ATTR = None
DEVICE_CLASS = None DEVICE_CLASS = None
def __init__(self, unique_id, zha_device, channels, **kwargs): def __init__(self, unique_id, zha_device, channels, **kwargs):
@@ -105,6 +106,8 @@ class BinarySensor(ZhaEntity, BinarySensorDevice):
@callback @callback
def async_set_state(self, attr_id, attr_name, value): def async_set_state(self, attr_id, attr_name, value):
"""Set the state.""" """Set the state."""
if self.SENSOR_ATTR is None or self.SENSOR_ATTR != attr_name:
return
self._state = bool(value) self._state = bool(value)
self.async_write_ha_state() self.async_write_ha_state()
@@ -121,6 +124,7 @@ class BinarySensor(ZhaEntity, BinarySensorDevice):
class Accelerometer(BinarySensor): class Accelerometer(BinarySensor):
"""ZHA BinarySensor.""" """ZHA BinarySensor."""
SENSOR_ATTR = "acceleration"
DEVICE_CLASS = DEVICE_CLASS_MOVING DEVICE_CLASS = DEVICE_CLASS_MOVING
@@ -128,6 +132,7 @@ class Accelerometer(BinarySensor):
class Occupancy(BinarySensor): class Occupancy(BinarySensor):
"""ZHA BinarySensor.""" """ZHA BinarySensor."""
SENSOR_ATTR = "occupancy"
DEVICE_CLASS = DEVICE_CLASS_OCCUPANCY DEVICE_CLASS = DEVICE_CLASS_OCCUPANCY
@@ -135,6 +140,7 @@ class Occupancy(BinarySensor):
class Opening(BinarySensor): class Opening(BinarySensor):
"""ZHA BinarySensor.""" """ZHA BinarySensor."""
SENSOR_ATTR = "on_off"
DEVICE_CLASS = DEVICE_CLASS_OPENING DEVICE_CLASS = DEVICE_CLASS_OPENING
@@ -142,6 +148,8 @@ class Opening(BinarySensor):
class IASZone(BinarySensor): class IASZone(BinarySensor):
"""ZHA IAS BinarySensor.""" """ZHA IAS BinarySensor."""
SENSOR_ATTR = "zone_status"
async def get_device_class(self) -> None: async def get_device_class(self) -> None:
"""Get the HA device class from the channel.""" """Get the HA device class from the channel."""
zone_type = await self._channel.get_attribute_value("zone_type") zone_type = await self._channel.get_attribute_value("zone_type")

View File

@@ -5,7 +5,7 @@
"documentation": "https://www.home-assistant.io/integrations/zha", "documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [ "requirements": [
"bellows-homeassistant==0.14.0", "bellows-homeassistant==0.14.0",
"zha-quirks==0.0.36", "zha-quirks==0.0.37",
"zigpy-cc==0.1.0", "zigpy-cc==0.1.0",
"zigpy-deconz==0.7.0", "zigpy-deconz==0.7.0",
"zigpy-homeassistant==0.16.0", "zigpy-homeassistant==0.16.0",

View File

@@ -83,6 +83,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class Sensor(ZhaEntity): class Sensor(ZhaEntity):
"""Base ZHA sensor.""" """Base ZHA sensor."""
SENSOR_ATTR = None
_decimals = 1 _decimals = 1
_device_class = None _device_class = None
_divisor = 1 _divisor = 1
@@ -126,6 +127,8 @@ class Sensor(ZhaEntity):
@callback @callback
def async_set_state(self, attr_id, attr_name, value): def async_set_state(self, attr_id, attr_name, value):
"""Handle state update from channel.""" """Handle state update from channel."""
if self.SENSOR_ATTR is None or self.SENSOR_ATTR != attr_name:
return
if value is not None: if value is not None:
value = self.formatter(value) value = self.formatter(value)
self._state = value self._state = value
@@ -154,6 +157,7 @@ class Sensor(ZhaEntity):
class AnalogInput(Sensor): class AnalogInput(Sensor):
"""Sensor that displays analog input values.""" """Sensor that displays analog input values."""
SENSOR_ATTR = "present_value"
pass pass
@@ -161,6 +165,7 @@ class AnalogInput(Sensor):
class Battery(Sensor): class Battery(Sensor):
"""Battery sensor of power configuration cluster.""" """Battery sensor of power configuration cluster."""
SENSOR_ATTR = "battery_percentage_remaining"
_device_class = DEVICE_CLASS_BATTERY _device_class = DEVICE_CLASS_BATTERY
_unit = UNIT_PERCENTAGE _unit = UNIT_PERCENTAGE
@@ -198,6 +203,7 @@ class Battery(Sensor):
class ElectricalMeasurement(Sensor): class ElectricalMeasurement(Sensor):
"""Active power measurement.""" """Active power measurement."""
SENSOR_ATTR = "active_power"
_device_class = DEVICE_CLASS_POWER _device_class = DEVICE_CLASS_POWER
_divisor = 10 _divisor = 10
_unit = POWER_WATT _unit = POWER_WATT
@@ -232,6 +238,7 @@ class Text(Sensor):
class Humidity(Sensor): class Humidity(Sensor):
"""Humidity sensor.""" """Humidity sensor."""
SENSOR_ATTR = "measured_value"
_device_class = DEVICE_CLASS_HUMIDITY _device_class = DEVICE_CLASS_HUMIDITY
_divisor = 100 _divisor = 100
_unit = UNIT_PERCENTAGE _unit = UNIT_PERCENTAGE
@@ -241,6 +248,7 @@ class Humidity(Sensor):
class Illuminance(Sensor): class Illuminance(Sensor):
"""Illuminance Sensor.""" """Illuminance Sensor."""
SENSOR_ATTR = "measured_value"
_device_class = DEVICE_CLASS_ILLUMINANCE _device_class = DEVICE_CLASS_ILLUMINANCE
_unit = "lx" _unit = "lx"
@@ -254,6 +262,7 @@ class Illuminance(Sensor):
class SmartEnergyMetering(Sensor): class SmartEnergyMetering(Sensor):
"""Metering sensor.""" """Metering sensor."""
SENSOR_ATTR = "instantaneous_demand"
_device_class = DEVICE_CLASS_POWER _device_class = DEVICE_CLASS_POWER
def formatter(self, value): def formatter(self, value):
@@ -270,6 +279,7 @@ class SmartEnergyMetering(Sensor):
class Pressure(Sensor): class Pressure(Sensor):
"""Pressure sensor.""" """Pressure sensor."""
SENSOR_ATTR = "measured_value"
_device_class = DEVICE_CLASS_PRESSURE _device_class = DEVICE_CLASS_PRESSURE
_decimals = 0 _decimals = 0
_unit = "hPa" _unit = "hPa"
@@ -279,6 +289,7 @@ class Pressure(Sensor):
class Temperature(Sensor): class Temperature(Sensor):
"""Temperature Sensor.""" """Temperature Sensor."""
SENSOR_ATTR = "measured_value"
_device_class = DEVICE_CLASS_TEMPERATURE _device_class = DEVICE_CLASS_TEMPERATURE
_divisor = 100 _divisor = 100
_unit = TEMP_CELSIUS _unit = TEMP_CELSIUS

View File

@@ -3,7 +3,7 @@
"name": "Z-Wave", "name": "Z-Wave",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave", "documentation": "https://www.home-assistant.io/integrations/zwave",
"requirements": ["homeassistant-pyozw==0.1.8", "pydispatcher==2.0.5"], "requirements": ["homeassistant-pyozw==0.1.9", "pydispatcher==2.0.5"],
"dependencies": [], "dependencies": [],
"codeowners": ["@home-assistant/z-wave"] "codeowners": ["@home-assistant/z-wave"]
} }

View File

@@ -1,7 +1,7 @@
"""Constants used by Home Assistant components.""" """Constants used by Home Assistant components."""
MAJOR_VERSION = 0 MAJOR_VERSION = 0
MINOR_VERSION = 107 MINOR_VERSION = 107
PATCH_VERSION = "0b3" PATCH_VERSION = "0"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 0) REQUIRED_PYTHON_VER = (3, 7, 0)

View File

@@ -12,7 +12,7 @@ cryptography==2.8
defusedxml==0.6.0 defusedxml==0.6.0
distro==1.4.0 distro==1.4.0
hass-nabucasa==0.32.2 hass-nabucasa==0.32.2
home-assistant-frontend==20200313.0 home-assistant-frontend==20200318.0
importlib-metadata==1.5.0 importlib-metadata==1.5.0
jinja2>=2.10.3 jinja2>=2.10.3
netdisco==2.6.0 netdisco==2.6.0

View File

@@ -163,7 +163,7 @@ aioftp==0.12.0
aioharmony==0.1.13 aioharmony==0.1.13
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit[IP]==0.2.29 aiohomekit[IP]==0.2.29.1
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
@@ -469,7 +469,7 @@ dweepy==0.3.0
dynalite_devices==0.1.32 dynalite_devices==0.1.32
# homeassistant.components.rainforest_eagle # homeassistant.components.rainforest_eagle
eagle200_reader==0.2.1 eagle200_reader==0.2.4
# homeassistant.components.ebusd # homeassistant.components.ebusd
ebusdpy==0.0.16 ebusdpy==0.0.16
@@ -696,10 +696,10 @@ hole==0.5.0
holidays==0.10.1 holidays==0.10.1
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20200313.0 home-assistant-frontend==20200318.0
# homeassistant.components.zwave # homeassistant.components.zwave
homeassistant-pyozw==0.1.8 homeassistant-pyozw==0.1.9
# homeassistant.components.homematicip_cloud # homeassistant.components.homematicip_cloud
homematicip==0.10.17 homematicip==0.10.17
@@ -747,7 +747,7 @@ incomfort-client==0.4.0
influxdb==5.2.3 influxdb==5.2.3
# homeassistant.components.insteon # homeassistant.components.insteon
insteonplm==0.16.7 insteonplm==0.16.8
# homeassistant.components.iperf3 # homeassistant.components.iperf3
iperf3==0.1.11 iperf3==0.1.11
@@ -1188,7 +1188,7 @@ pycfdns==0.0.1
pychannels==1.0.0 pychannels==1.0.0
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==4.1.1 pychromecast==4.2.0
# homeassistant.components.cmus # homeassistant.components.cmus
pycmus==0.1.1 pycmus==0.1.1
@@ -1315,7 +1315,7 @@ pyhomeworks==0.0.6
pyialarm==0.3 pyialarm==0.3
# homeassistant.components.icloud # homeassistant.components.icloud
pyicloud==0.9.4 pyicloud==0.9.5
# homeassistant.components.intesishome # homeassistant.components.intesishome
pyintesishome==1.6 pyintesishome==1.6
@@ -1996,7 +1996,7 @@ temperusb==1.5.3
# tensorflow==1.13.2 # tensorflow==1.13.2
# homeassistant.components.tesla # homeassistant.components.tesla
teslajsonpy==0.4.0 teslajsonpy==0.5.1
# homeassistant.components.thermoworks_smoke # homeassistant.components.thermoworks_smoke
thermoworks_smoke==0.1.8 thermoworks_smoke==0.1.8
@@ -2158,7 +2158,7 @@ zengge==0.2
zeroconf==0.24.5 zeroconf==0.24.5
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.36 zha-quirks==0.0.37
# homeassistant.components.zhong_hong # homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9 zhong_hong_hvac==1.0.9

View File

@@ -62,7 +62,7 @@ aiobotocore==0.11.1
aioesphomeapi==2.6.1 aioesphomeapi==2.6.1
# homeassistant.components.homekit_controller # homeassistant.components.homekit_controller
aiohomekit[IP]==0.2.29 aiohomekit[IP]==0.2.29.1
# homeassistant.components.emulated_hue # homeassistant.components.emulated_hue
# homeassistant.components.http # homeassistant.components.http
@@ -263,10 +263,10 @@ hole==0.5.0
holidays==0.10.1 holidays==0.10.1
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20200313.0 home-assistant-frontend==20200318.0
# homeassistant.components.zwave # homeassistant.components.zwave
homeassistant-pyozw==0.1.8 homeassistant-pyozw==0.1.9
# homeassistant.components.homematicip_cloud # homeassistant.components.homematicip_cloud
homematicip==0.10.17 homematicip==0.10.17
@@ -443,7 +443,7 @@ pyblackbird==0.5
pybotvac==0.0.17 pybotvac==0.0.17
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==4.1.1 pychromecast==4.2.0
# homeassistant.components.coolmaster # homeassistant.components.coolmaster
pycoolmasternet==0.0.4 pycoolmasternet==0.0.4
@@ -483,7 +483,7 @@ pyheos==0.6.0
pyhomematic==0.1.65 pyhomematic==0.1.65
# homeassistant.components.icloud # homeassistant.components.icloud
pyicloud==0.9.4 pyicloud==0.9.5
# homeassistant.components.ipma # homeassistant.components.ipma
pyipma==2.0.5 pyipma==2.0.5
@@ -684,7 +684,7 @@ sunwatcher==0.2.1
tellduslive==0.10.10 tellduslive==0.10.10
# homeassistant.components.tesla # homeassistant.components.tesla
teslajsonpy==0.4.0 teslajsonpy==0.5.1
# homeassistant.components.toon # homeassistant.components.toon
toonapilib==3.2.4 toonapilib==3.2.4
@@ -747,7 +747,7 @@ yahooweather==0.10
zeroconf==0.24.5 zeroconf==0.24.5
# homeassistant.components.zha # homeassistant.components.zha
zha-quirks==0.0.36 zha-quirks==0.0.37
# homeassistant.components.zha # homeassistant.components.zha
zigpy-cc==0.1.0 zigpy-cc==0.1.0

View File

@@ -156,7 +156,7 @@ def calc_allowed_references(integration: Integration) -> Set[str]:
"""Return a set of allowed references.""" """Return a set of allowed references."""
allowed_references = ( allowed_references = (
ALLOWED_USED_COMPONENTS ALLOWED_USED_COMPONENTS
| set(integration.manifest["dependencies"]) | set(integration.manifest.get("dependencies", []))
| set(integration.manifest.get("after_dependencies", [])) | set(integration.manifest.get("after_dependencies", []))
) )
@@ -250,7 +250,7 @@ def validate(integrations: Dict[str, Integration], config):
validate_dependencies(integrations, integration) validate_dependencies(integrations, integration)
# check that all referenced dependencies exist # check that all referenced dependencies exist
for dep in integration.manifest["dependencies"]: for dep in integration.manifest.get("dependencies", []):
if dep not in integrations: if dep not in integrations:
integration.add_error( integration.add_error(
"dependencies", f"Dependency {dep} does not exist" "dependencies", f"Dependency {dep} does not exist"

View File

@@ -52,8 +52,8 @@ MANIFEST_SCHEMA = vol.Schema(
vol.Url(), documentation_url # pylint: disable=no-value-for-parameter vol.Url(), documentation_url # pylint: disable=no-value-for-parameter
), ),
vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES), vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES),
vol.Required("requirements"): [str], vol.Optional("requirements"): [str],
vol.Required("dependencies"): [str], vol.Optional("dependencies"): [str],
vol.Optional("after_dependencies"): [str], vol.Optional("after_dependencies"): [str],
vol.Required("codeowners"): [str], vol.Required("codeowners"): [str],
vol.Optional("logo"): vol.Url(), # pylint: disable=no-value-for-parameter vol.Optional("logo"): vol.Url(), # pylint: disable=no-value-for-parameter

View File

@@ -1,4 +1,6 @@
"""Tests for the DirecTV component.""" """Tests for the DirecTV component."""
from DirectPy import DIRECTV
from homeassistant.components.directv.const import DOMAIN from homeassistant.components.directv.const import DOMAIN
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
@@ -94,18 +96,23 @@ MOCK_GET_VERSION = {
} }
class MockDirectvClass: class MockDirectvClass(DIRECTV):
"""A fake DirecTV DVR device.""" """A fake DirecTV DVR device."""
def __init__(self, ip, port=8080, clientAddr="0"): def __init__(self, ip, port=8080, clientAddr="0", determine_state=False):
"""Initialize the fake DirecTV device.""" """Initialize the fake DirecTV device."""
self._host = ip super().__init__(
self._port = port ip=ip, port=port, clientAddr=clientAddr, determine_state=determine_state,
self._device = clientAddr )
self._standby = True
self._play = False
self.attributes = LIVE self._play = False
self._standby = True
if self.clientAddr == CLIENT_ADDRESS:
self.attributes = RECORDING
self._standby = False
else:
self.attributes = LIVE
def get_locations(self): def get_locations(self):
"""Mock for get_locations method.""" """Mock for get_locations method."""

View File

@@ -114,9 +114,7 @@ async def test_form_cannot_connect(hass: HomeAssistantType) -> None:
) )
with patch( with patch(
"homeassistant.components.directv.config_flow.DIRECTV", new=MockDirectvClass, "tests.components.directv.test_config_flow.MockDirectvClass.get_version",
), patch(
"homeassistant.components.directv.config_flow.DIRECTV.get_version",
side_effect=RequestException, side_effect=RequestException,
) as mock_validate_input: ) as mock_validate_input:
result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},)
@@ -135,15 +133,13 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None:
) )
with patch( with patch(
"homeassistant.components.directv.config_flow.DIRECTV", new=MockDirectvClass, "tests.components.directv.test_config_flow.MockDirectvClass.get_version",
), patch(
"homeassistant.components.directv.config_flow.DIRECTV.get_version",
side_effect=Exception, side_effect=Exception,
) as mock_validate_input: ) as mock_validate_input:
result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},)
assert result["type"] == RESULT_TYPE_FORM assert result["type"] == RESULT_TYPE_ABORT
assert result["errors"] == {"base": "unknown"} assert result["reason"] == "unknown"
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_validate_input.mock_calls) == 1 assert len(mock_validate_input.mock_calls) == 1
@@ -205,9 +201,7 @@ async def test_ssdp_discovery_confirm_abort(hass: HomeAssistantType) -> None:
) )
with patch( with patch(
"homeassistant.components.directv.config_flow.DIRECTV", new=MockDirectvClass, "tests.components.directv.test_config_flow.MockDirectvClass.get_version",
), patch(
"homeassistant.components.directv.config_flow.DIRECTV.get_version",
side_effect=RequestException, side_effect=RequestException,
) as mock_validate_input: ) as mock_validate_input:
result = await async_configure_flow(hass, result["flow_id"], {}) result = await async_configure_flow(hass, result["flow_id"], {})
@@ -227,9 +221,7 @@ async def test_ssdp_discovery_confirm_unknown_error(hass: HomeAssistantType) ->
) )
with patch( with patch(
"homeassistant.components.directv.config_flow.DIRECTV", new=MockDirectvClass, "tests.components.directv.test_config_flow.MockDirectvClass.get_version",
), patch(
"homeassistant.components.directv.config_flow.DIRECTV.get_version",
side_effect=Exception, side_effect=Exception,
) as mock_validate_input: ) as mock_validate_input:
result = await async_configure_flow(hass, result["flow_id"], {}) result = await async_configure_flow(hass, result["flow_id"], {})

View File

@@ -54,9 +54,7 @@ from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.directv import ( from tests.components.directv import (
CLIENT_ADDRESS,
DOMAIN, DOMAIN,
HOST,
MOCK_GET_LOCATIONS_MULTIPLE, MOCK_GET_LOCATIONS_MULTIPLE,
RECORDING, RECORDING,
MockDirectvClass, MockDirectvClass,
@@ -70,15 +68,6 @@ MAIN_ENTITY_ID = f"{MP_DOMAIN}.main_dvr"
# pylint: disable=redefined-outer-name # pylint: disable=redefined-outer-name
@fixture
def client_dtv() -> MockDirectvClass:
"""Fixture for a client device."""
mocked_dtv = MockDirectvClass(HOST, clientAddr=CLIENT_ADDRESS)
mocked_dtv.attributes = RECORDING
mocked_dtv._standby = False # pylint: disable=protected-access
return mocked_dtv
@fixture @fixture
def mock_now() -> datetime: def mock_now() -> datetime:
"""Fixture for dtutil.now.""" """Fixture for dtutil.now."""
@@ -93,34 +82,19 @@ async def setup_directv(hass: HomeAssistantType) -> MockConfigEntry:
return await setup_integration(hass) return await setup_integration(hass)
async def setup_directv_with_instance_error(hass: HomeAssistantType) -> MockConfigEntry: async def setup_directv_with_locations(hass: HomeAssistantType) -> MockConfigEntry:
"""Set up mock DirecTV integration.""" """Set up mock DirecTV integration."""
with patch( with patch(
"homeassistant.components.directv.DIRECTV", new=MockDirectvClass, "tests.components.directv.test_media_player.MockDirectvClass.get_locations",
), patch(
"homeassistant.components.directv.DIRECTV.get_locations",
return_value=MOCK_GET_LOCATIONS_MULTIPLE, return_value=MOCK_GET_LOCATIONS_MULTIPLE,
), patch(
"homeassistant.components.directv.media_player.get_dtv_instance",
return_value=None,
): ):
return await setup_integration(hass) with patch(
"homeassistant.components.directv.DIRECTV", new=MockDirectvClass,
), patch(
async def setup_directv_with_locations( "homeassistant.components.directv.media_player.DIRECTV",
hass: HomeAssistantType, client_dtv: MockDirectvClass, new=MockDirectvClass,
) -> MockConfigEntry: ):
"""Set up mock DirecTV integration.""" return await setup_integration(hass)
with patch(
"homeassistant.components.directv.DIRECTV", new=MockDirectvClass,
), patch(
"homeassistant.components.directv.DIRECTV.get_locations",
return_value=MOCK_GET_LOCATIONS_MULTIPLE,
), patch(
"homeassistant.components.directv.media_player.get_dtv_instance",
return_value=client_dtv,
):
return await setup_integration(hass)
async def async_turn_on( async def async_turn_on(
@@ -204,27 +178,17 @@ async def test_setup(hass: HomeAssistantType) -> None:
assert hass.states.get(MAIN_ENTITY_ID) assert hass.states.get(MAIN_ENTITY_ID)
async def test_setup_with_multiple_locations( async def test_setup_with_multiple_locations(hass: HomeAssistantType) -> None:
hass: HomeAssistantType, client_dtv: MockDirectvClass
) -> None:
"""Test setup with basic config with client location.""" """Test setup with basic config with client location."""
await setup_directv_with_locations(hass, client_dtv) await setup_directv_with_locations(hass)
assert hass.states.get(MAIN_ENTITY_ID) assert hass.states.get(MAIN_ENTITY_ID)
assert hass.states.get(CLIENT_ENTITY_ID) assert hass.states.get(CLIENT_ENTITY_ID)
async def test_setup_with_instance_error(hass: HomeAssistantType) -> None: async def test_unique_id(hass: HomeAssistantType) -> None:
"""Test setup with basic config with client location that results in instance error."""
await setup_directv_with_instance_error(hass)
assert hass.states.get(MAIN_ENTITY_ID)
assert hass.states.async_entity_ids(MP_DOMAIN) == [MAIN_ENTITY_ID]
async def test_unique_id(hass: HomeAssistantType, client_dtv: MockDirectvClass) -> None:
"""Test unique id.""" """Test unique id."""
await setup_directv_with_locations(hass, client_dtv) await setup_directv_with_locations(hass)
entity_registry = await hass.helpers.entity_registry.async_get_registry() entity_registry = await hass.helpers.entity_registry.async_get_registry()
@@ -235,11 +199,9 @@ async def test_unique_id(hass: HomeAssistantType, client_dtv: MockDirectvClass)
assert client.unique_id == "2CA17D1CD30X" assert client.unique_id == "2CA17D1CD30X"
async def test_supported_features( async def test_supported_features(hass: HomeAssistantType) -> None:
hass: HomeAssistantType, client_dtv: MockDirectvClass
) -> None:
"""Test supported features.""" """Test supported features."""
await setup_directv_with_locations(hass, client_dtv) await setup_directv_with_locations(hass)
# Features supported for main DVR # Features supported for main DVR
state = hass.states.get(MAIN_ENTITY_ID) state = hass.states.get(MAIN_ENTITY_ID)
@@ -269,10 +231,10 @@ async def test_supported_features(
async def test_check_attributes( async def test_check_attributes(
hass: HomeAssistantType, mock_now: dt_util.dt.datetime, client_dtv: MockDirectvClass hass: HomeAssistantType, mock_now: dt_util.dt.datetime
) -> None: ) -> None:
"""Test attributes.""" """Test attributes."""
await setup_directv_with_locations(hass, client_dtv) await setup_directv_with_locations(hass)
next_update = mock_now + timedelta(minutes=5) next_update = mock_now + timedelta(minutes=5)
with patch("homeassistant.util.dt.utcnow", return_value=next_update): with patch("homeassistant.util.dt.utcnow", return_value=next_update):
@@ -321,10 +283,10 @@ async def test_check_attributes(
async def test_main_services( async def test_main_services(
hass: HomeAssistantType, mock_now: dt_util.dt.datetime, client_dtv: MockDirectvClass hass: HomeAssistantType, mock_now: dt_util.dt.datetime
) -> None: ) -> None:
"""Test the different services.""" """Test the different services."""
await setup_directv_with_locations(hass, client_dtv) await setup_directv(hass)
next_update = mock_now + timedelta(minutes=5) next_update = mock_now + timedelta(minutes=5)
with patch("homeassistant.util.dt.utcnow", return_value=next_update): with patch("homeassistant.util.dt.utcnow", return_value=next_update):
@@ -373,10 +335,10 @@ async def test_main_services(
async def test_available( async def test_available(
hass: HomeAssistantType, mock_now: dt_util.dt.datetime, client_dtv: MockDirectvClass hass: HomeAssistantType, mock_now: dt_util.dt.datetime
) -> None: ) -> None:
"""Test available status.""" """Test available status."""
entry = await setup_directv_with_locations(hass, client_dtv) entry = await setup_directv(hass)
next_update = mock_now + timedelta(minutes=5) next_update = mock_now + timedelta(minutes=5)
with patch("homeassistant.util.dt.utcnow", return_value=next_update): with patch("homeassistant.util.dt.utcnow", return_value=next_update):

View File

@@ -1,5 +1,5 @@
"""Initializer helpers for HomematicIP fake server.""" """Initializer helpers for HomematicIP fake server."""
from asynctest import CoroutineMock, MagicMock, Mock from asynctest import CoroutineMock, MagicMock, Mock, patch
from homematicip.aio.auth import AsyncAuth from homematicip.aio.auth import AsyncAuth
from homematicip.aio.connection import AsyncConnection from homematicip.aio.connection import AsyncConnection
from homematicip.aio.home import AsyncHome from homematicip.aio.home import AsyncHome
@@ -106,9 +106,10 @@ async def mock_hap_with_service_fixture(
@pytest.fixture(name="simple_mock_home") @pytest.fixture(name="simple_mock_home")
def simple_mock_home_fixture() -> AsyncHome: def simple_mock_home_fixture():
"""Return a simple AsyncHome Mock.""" """Return a simple mocked connection."""
return Mock(
mock_home = Mock(
spec=AsyncHome, spec=AsyncHome,
name="Demo", name="Demo",
devices=[], devices=[],
@@ -120,6 +121,27 @@ def simple_mock_home_fixture() -> AsyncHome:
connected=True, connected=True,
) )
with patch(
"homeassistant.components.homematicip_cloud.hap.AsyncHome",
autospec=True,
return_value=mock_home,
):
yield
@pytest.fixture(name="mock_connection_init")
def mock_connection_init_fixture():
"""Return a simple mocked connection."""
with patch(
"homeassistant.components.homematicip_cloud.hap.AsyncHome.init",
return_value=None,
), patch(
"homeassistant.components.homematicip_cloud.hap.AsyncAuth.init",
return_value=None,
):
yield
@pytest.fixture(name="simple_mock_auth") @pytest.fixture(name="simple_mock_auth")
def simple_mock_auth_fixture() -> AsyncAuth: def simple_mock_auth_fixture() -> AsyncAuth:

View File

@@ -31,9 +31,7 @@ async def _async_manipulate_security_zones(
internal_zone = home.search_group_by_id(internal_zone_id) internal_zone = home.search_group_by_id(internal_zone_id)
internal_zone.active = internal_active internal_zone.active = internal_active
home.from_json(json) home.update_home_only(json)
home._get_functionalHomes(json)
home._load_functionalChannels()
home.fire_update_event(json) home.fire_update_event(json)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@@ -16,12 +16,15 @@ DEFAULT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"}
IMPORT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"} IMPORT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"}
async def test_flow_works(hass): async def test_flow_works(hass, simple_mock_home):
"""Test config flow.""" """Test config flow."""
with patch( with patch(
"homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton",
return_value=False, return_value=False,
), patch(
"homeassistant.components.homematicip_cloud.hap.HomematicipAuth.get_auth",
return_value=True,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG
@@ -137,7 +140,7 @@ async def test_init_already_configured(hass):
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
async def test_import_config(hass): async def test_import_config(hass, simple_mock_home):
"""Test importing a host with an existing config file.""" """Test importing a host with an existing config file."""
with patch( with patch(
"homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton",

View File

@@ -125,14 +125,11 @@ async def test_hap_create(hass, hmip_config_entry, simple_mock_home):
hass.config.components.add(HMIPC_DOMAIN) hass.config.components.add(HMIPC_DOMAIN)
hap = HomematicipHAP(hass, hmip_config_entry) hap = HomematicipHAP(hass, hmip_config_entry)
assert hap assert hap
with patch( with patch.object(hap, "async_connect"):
"homeassistant.components.homematicip_cloud.hap.AsyncHome",
return_value=simple_mock_home,
), patch.object(hap, "async_connect"):
assert await hap.async_setup() assert await hap.async_setup()
async def test_hap_create_exception(hass, hmip_config_entry): async def test_hap_create_exception(hass, hmip_config_entry, mock_connection_init):
"""Mock AsyncHome to execute get_hap.""" """Mock AsyncHome to execute get_hap."""
hass.config.components.add(HMIPC_DOMAIN) hass.config.components.add(HMIPC_DOMAIN)

View File

@@ -24,7 +24,9 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
async def test_config_with_accesspoint_passed_to_config_entry(hass): async def test_config_with_accesspoint_passed_to_config_entry(
hass, mock_connection, simple_mock_home
):
"""Test that config for a accesspoint are loaded via config entry.""" """Test that config for a accesspoint are loaded via config entry."""
entry_config = { entry_config = {
@@ -51,7 +53,9 @@ async def test_config_with_accesspoint_passed_to_config_entry(hass):
assert isinstance(hass.data[HMIPC_DOMAIN]["ABC123"], HomematicipHAP) assert isinstance(hass.data[HMIPC_DOMAIN]["ABC123"], HomematicipHAP)
async def test_config_already_registered_not_passed_to_config_entry(hass): async def test_config_already_registered_not_passed_to_config_entry(
hass, simple_mock_home
):
"""Test that an already registered accesspoint does not get imported.""" """Test that an already registered accesspoint does not get imported."""
mock_config = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC123", HMIPC_NAME: "name"} mock_config = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC123", HMIPC_NAME: "name"}
@@ -87,7 +91,9 @@ async def test_config_already_registered_not_passed_to_config_entry(hass):
assert config_entries[0].unique_id == "ABC123" assert config_entries[0].unique_id == "ABC123"
async def test_load_entry_fails_due_to_connection_error(hass, hmip_config_entry): async def test_load_entry_fails_due_to_connection_error(
hass, hmip_config_entry, mock_connection_init
):
"""Test load entry fails due to connection error.""" """Test load entry fails due to connection error."""
hmip_config_entry.add_to_hass(hass) hmip_config_entry.add_to_hass(hass)
@@ -101,7 +107,9 @@ async def test_load_entry_fails_due_to_connection_error(hass, hmip_config_entry)
assert hmip_config_entry.state == ENTRY_STATE_SETUP_RETRY assert hmip_config_entry.state == ENTRY_STATE_SETUP_RETRY
async def test_load_entry_fails_due_to_generic_exception(hass, hmip_config_entry): async def test_load_entry_fails_due_to_generic_exception(
hass, hmip_config_entry, simple_mock_home
):
"""Test load entry fails due to generic exception.""" """Test load entry fails due to generic exception."""
hmip_config_entry.add_to_hass(hass) hmip_config_entry.add_to_hass(hass)

View File

@@ -582,6 +582,10 @@ async def test_state_updates(hass, aiohttp_client, mock_panel):
) )
entry.add_to_hass(hass) entry.add_to_hass(hass)
# Add empty data field to ensure we process it correctly (possible if entry is ignored)
entry = MockConfigEntry(domain="konnected", title="Konnected Alarm Panel", data={},)
entry.add_to_hass(hass)
assert ( assert (
await async_setup_component( await async_setup_component(
hass, hass,

View File

@@ -126,7 +126,7 @@ async def test_get_action_capabilities_brightness(hass, device_reg, entity_reg):
expected_capabilities = { expected_capabilities = {
"extra_fields": [ "extra_fields": [
{ {
"name": "brightness", "name": "brightness_pct",
"optional": True, "optional": True,
"type": "integer", "type": "integer",
"valueMax": 100, "valueMax": 100,
@@ -218,7 +218,7 @@ async def test_action(hass, calls):
"device_id": "", "device_id": "",
"entity_id": ent1.entity_id, "entity_id": ent1.entity_id,
"type": "turn_on", "type": "turn_on",
"brightness": 75, "brightness_pct": 75,
}, },
}, },
] ]
@@ -273,11 +273,11 @@ async def test_action(hass, calls):
assert len(turn_on_calls) == 3 assert len(turn_on_calls) == 3
assert turn_on_calls[2].data["entity_id"] == ent1.entity_id assert turn_on_calls[2].data["entity_id"] == ent1.entity_id
assert turn_on_calls[2].data["brightness"] == 75 assert turn_on_calls[2].data["brightness_pct"] == 75
hass.bus.async_fire("test_on") hass.bus.async_fire("test_on")
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(turn_on_calls) == 4 assert len(turn_on_calls) == 4
assert turn_on_calls[3].data["entity_id"] == ent1.entity_id assert turn_on_calls[3].data["entity_id"] == ent1.entity_id
assert "brightness" not in turn_on_calls[3].data assert "brightness_pct" not in turn_on_calls[3].data

View File

@@ -5,10 +5,13 @@ import pytest
from homeassistant.components import frontend from homeassistant.components import frontend
from homeassistant.components.lovelace import const, dashboard from homeassistant.components.lovelace import const, dashboard
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import async_capture_events, get_system_health_info from tests.common import (
assert_setup_component,
async_capture_events,
get_system_health_info,
)
async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage): async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage):
@@ -224,8 +227,6 @@ async def test_dashboard_from_yaml(hass, hass_ws_client, url_path):
} }
}, },
) )
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert hass.data[frontend.DATA_PANELS]["test-panel"].config == {"mode": "yaml"} assert hass.data[frontend.DATA_PANELS]["test-panel"].config == {"mode": "yaml"}
assert hass.data[frontend.DATA_PANELS]["test-panel-no-sidebar"].config == { assert hass.data[frontend.DATA_PANELS]["test-panel-no-sidebar"].config == {
"mode": "yaml" "mode": "yaml"
@@ -306,11 +307,32 @@ async def test_dashboard_from_yaml(hass, hass_ws_client, url_path):
assert len(events) == 1 assert len(events) == 1
async def test_wrong_key_dashboard_from_yaml(hass):
"""Test we don't load lovelace dashboard without hyphen config from yaml."""
with assert_setup_component(0):
assert not await async_setup_component(
hass,
"lovelace",
{
"lovelace": {
"dashboards": {
"testpanel": {
"mode": "yaml",
"filename": "bla.yaml",
"title": "Test Panel",
"icon": "mdi:test-icon",
"show_in_sidebar": False,
"require_admin": True,
}
}
}
},
)
async def test_storage_dashboards(hass, hass_ws_client, hass_storage): async def test_storage_dashboards(hass, hass_ws_client, hass_storage):
"""Test we load lovelace config from storage.""" """Test we load lovelace config from storage."""
assert await async_setup_component(hass, "lovelace", {}) assert await async_setup_component(hass, "lovelace", {})
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert hass.data[frontend.DATA_PANELS]["lovelace"].config == {"mode": "storage"} assert hass.data[frontend.DATA_PANELS]["lovelace"].config == {"mode": "storage"}
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
@@ -321,12 +343,24 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage):
assert response["success"] assert response["success"]
assert response["result"] == [] assert response["result"] == []
# Add a dashboard # Add a wrong dashboard
await client.send_json( await client.send_json(
{ {
"id": 6, "id": 6,
"type": "lovelace/dashboards/create", "type": "lovelace/dashboards/create",
"url_path": "created_url_path", "url_path": "path",
"title": "Test path without hyphen",
}
)
response = await client.receive_json()
assert not response["success"]
# Add a dashboard
await client.send_json(
{
"id": 7,
"type": "lovelace/dashboards/create",
"url_path": "created-url-path",
"require_admin": True, "require_admin": True,
"title": "New Title", "title": "New Title",
"icon": "mdi:map", "icon": "mdi:map",
@@ -340,9 +374,9 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage):
dashboard_id = response["result"]["id"] dashboard_id = response["result"]["id"]
assert "created_url_path" in hass.data[frontend.DATA_PANELS] assert "created-url-path" in hass.data[frontend.DATA_PANELS]
await client.send_json({"id": 7, "type": "lovelace/dashboards/list"}) await client.send_json({"id": 8, "type": "lovelace/dashboards/list"})
response = await client.receive_json() response = await client.receive_json()
assert response["success"] assert response["success"]
assert len(response["result"]) == 1 assert len(response["result"]) == 1
@@ -354,7 +388,7 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage):
# Fetch config # Fetch config
await client.send_json( await client.send_json(
{"id": 8, "type": "lovelace/config", "url_path": "created_url_path"} {"id": 9, "type": "lovelace/config", "url_path": "created-url-path"}
) )
response = await client.receive_json() response = await client.receive_json()
assert not response["success"] assert not response["success"]
@@ -365,9 +399,9 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage):
await client.send_json( await client.send_json(
{ {
"id": 9, "id": 10,
"type": "lovelace/config/save", "type": "lovelace/config/save",
"url_path": "created_url_path", "url_path": "created-url-path",
"config": {"yo": "hello"}, "config": {"yo": "hello"},
} }
) )
@@ -377,10 +411,10 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage):
"config": {"yo": "hello"} "config": {"yo": "hello"}
} }
assert len(events) == 1 assert len(events) == 1
assert events[0].data["url_path"] == "created_url_path" assert events[0].data["url_path"] == "created-url-path"
await client.send_json( await client.send_json(
{"id": 10, "type": "lovelace/config", "url_path": "created_url_path"} {"id": 11, "type": "lovelace/config", "url_path": "created-url-path"}
) )
response = await client.receive_json() response = await client.receive_json()
assert response["success"] assert response["success"]
@@ -389,7 +423,7 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage):
# Update a dashboard # Update a dashboard
await client.send_json( await client.send_json(
{ {
"id": 11, "id": 12,
"type": "lovelace/dashboards/update", "type": "lovelace/dashboards/update",
"dashboard_id": dashboard_id, "dashboard_id": dashboard_id,
"require_admin": False, "require_admin": False,
@@ -401,19 +435,19 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage):
response = await client.receive_json() response = await client.receive_json()
assert response["success"] assert response["success"]
assert response["result"]["mode"] == "storage" assert response["result"]["mode"] == "storage"
assert response["result"]["url_path"] == "created_url_path" assert response["result"]["url_path"] == "created-url-path"
assert response["result"]["title"] == "Updated Title" assert response["result"]["title"] == "Updated Title"
assert response["result"]["icon"] == "mdi:updated" assert response["result"]["icon"] == "mdi:updated"
assert response["result"]["show_in_sidebar"] is False assert response["result"]["show_in_sidebar"] is False
assert response["result"]["require_admin"] is False assert response["result"]["require_admin"] is False
# List dashboards again and make sure we see latest config # List dashboards again and make sure we see latest config
await client.send_json({"id": 12, "type": "lovelace/dashboards/list"}) await client.send_json({"id": 13, "type": "lovelace/dashboards/list"})
response = await client.receive_json() response = await client.receive_json()
assert response["success"] assert response["success"]
assert len(response["result"]) == 1 assert len(response["result"]) == 1
assert response["result"][0]["mode"] == "storage" assert response["result"][0]["mode"] == "storage"
assert response["result"][0]["url_path"] == "created_url_path" assert response["result"][0]["url_path"] == "created-url-path"
assert response["result"][0]["title"] == "Updated Title" assert response["result"][0]["title"] == "Updated Title"
assert response["result"][0]["icon"] == "mdi:updated" assert response["result"][0]["icon"] == "mdi:updated"
assert response["result"][0]["show_in_sidebar"] is False assert response["result"][0]["show_in_sidebar"] is False
@@ -421,22 +455,75 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage):
# Add dashboard with existing url path # Add dashboard with existing url path
await client.send_json( await client.send_json(
{"id": 13, "type": "lovelace/dashboards/create", "url_path": "created_url_path"} {"id": 14, "type": "lovelace/dashboards/create", "url_path": "created-url-path"}
) )
response = await client.receive_json() response = await client.receive_json()
assert not response["success"] assert not response["success"]
# Delete dashboards # Delete dashboards
await client.send_json( await client.send_json(
{"id": 14, "type": "lovelace/dashboards/delete", "dashboard_id": dashboard_id} {"id": 15, "type": "lovelace/dashboards/delete", "dashboard_id": dashboard_id}
) )
response = await client.receive_json() response = await client.receive_json()
assert response["success"] assert response["success"]
assert "created_url_path" not in hass.data[frontend.DATA_PANELS] assert "created-url-path" not in hass.data[frontend.DATA_PANELS]
assert dashboard.CONFIG_STORAGE_KEY.format(dashboard_id) not in hass_storage assert dashboard.CONFIG_STORAGE_KEY.format(dashboard_id) not in hass_storage
async def test_storage_dashboard_migrate(hass, hass_ws_client, hass_storage):
"""Test changing url path from storage config."""
hass_storage[dashboard.DASHBOARDS_STORAGE_KEY] = {
"key": "lovelace_dashboards",
"version": 1,
"data": {
"items": [
{
"icon": "mdi:tools",
"id": "tools",
"mode": "storage",
"require_admin": True,
"show_in_sidebar": True,
"title": "Tools",
"url_path": "tools",
},
{
"icon": "mdi:tools",
"id": "tools2",
"mode": "storage",
"require_admin": True,
"show_in_sidebar": True,
"title": "Tools",
"url_path": "dashboard-tools",
},
]
},
}
assert await async_setup_component(hass, "lovelace", {})
client = await hass_ws_client(hass)
# Fetch data
await client.send_json({"id": 5, "type": "lovelace/dashboards/list"})
response = await client.receive_json()
assert response["success"]
without_hyphen, with_hyphen = response["result"]
assert without_hyphen["icon"] == "mdi:tools"
assert without_hyphen["id"] == "tools"
assert without_hyphen["mode"] == "storage"
assert without_hyphen["require_admin"]
assert without_hyphen["show_in_sidebar"]
assert without_hyphen["title"] == "Tools"
assert without_hyphen["url_path"] == "lovelace-tools"
assert (
with_hyphen
== hass_storage[dashboard.DASHBOARDS_STORAGE_KEY]["data"]["items"][1]
)
async def test_websocket_list_dashboards(hass, hass_ws_client): async def test_websocket_list_dashboards(hass, hass_ws_client):
"""Test listing dashboards both storage + YAML.""" """Test listing dashboards both storage + YAML."""
assert await async_setup_component( assert await async_setup_component(
@@ -455,9 +542,6 @@ async def test_websocket_list_dashboards(hass, hass_ws_client):
}, },
) )
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
# Create a storage dashboard # Create a storage dashboard
@@ -465,7 +549,7 @@ async def test_websocket_list_dashboards(hass, hass_ws_client):
{ {
"id": 6, "id": 6,
"type": "lovelace/dashboards/create", "type": "lovelace/dashboards/create",
"url_path": "created_url_path", "url_path": "created-url-path",
"title": "Test Storage", "title": "Test Storage",
} }
) )
@@ -473,7 +557,7 @@ async def test_websocket_list_dashboards(hass, hass_ws_client):
assert response["success"] assert response["success"]
# List dashboards # List dashboards
await client.send_json({"id": 7, "type": "lovelace/dashboards/list"}) await client.send_json({"id": 8, "type": "lovelace/dashboards/list"})
response = await client.receive_json() response = await client.receive_json()
assert response["success"] assert response["success"]
assert len(response["result"]) == 2 assert len(response["result"]) == 2
@@ -486,4 +570,4 @@ async def test_websocket_list_dashboards(hass, hass_ws_client):
assert without_sb["mode"] == "storage" assert without_sb["mode"] == "storage"
assert without_sb["title"] == "Test Storage" assert without_sb["title"] == "Test Storage"
assert without_sb["url_path"] == "created_url_path" assert without_sb["url_path"] == "created-url-path"

View File

@@ -160,8 +160,10 @@ async def test_webhook_handle_get_zones(hass, create_registrations, webhook_clie
assert resp.status == 200 assert resp.status == 200
json = await resp.json() json = await resp.json()
assert len(json) == 1 assert len(json) == 2
assert json[0]["entity_id"] == "zone.home" zones = sorted(json, key=lambda entry: entry["entity_id"])
assert zones[0]["entity_id"] == "zone.home"
assert zones[1]["entity_id"] == "zone.test"
async def test_webhook_handle_get_config(hass, create_registrations, webhook_client): async def test_webhook_handle_get_config(hass, create_registrations, webhook_client):

View File

@@ -181,3 +181,17 @@ async def test_url_option_conflict(hass):
for config in to_try: for config in to_try:
result = await setup.async_setup_component(hass, "panel_custom", config) result = await setup.async_setup_component(hass, "panel_custom", config)
assert not result assert not result
async def test_url_path_conflict(hass):
"""Test config with overlapping url path."""
assert await setup.async_setup_component(
hass,
"panel_custom",
{
"panel_custom": [
{"name": "todo-mvc", "js_url": "/local/bla.js"},
{"name": "todo-mvc", "js_url": "/local/bla.js"},
]
},
)

View File

@@ -157,7 +157,7 @@ async def test_dedup_logs(hass, hass_client):
log_msg() log_msg()
log = await get_error_log(hass, hass_client, 3) log = await get_error_log(hass, hass_client, 3)
assert_log(log[0], "", ["error message 2", "error message 2-2"], "ERROR") assert_log(log[0], "", ["error message 2", "error message 2-2"], "ERROR")
assert log[0]["timestamp"] > log[0]["first_occured"] assert log[0]["timestamp"] > log[0]["first_occurred"]
log_msg("2-3") log_msg("2-3")
log_msg("2-4") log_msg("2-4")

View File

@@ -108,7 +108,7 @@ async def test_no_clients(hass):
"""Test the update_clients function when no clients are found.""" """Test the update_clients function when no clients are found."""
await setup_unifi_integration(hass) await setup_unifi_integration(hass)
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 0
async def test_tracked_devices(hass): async def test_tracked_devices(hass):
@@ -123,7 +123,7 @@ async def test_tracked_devices(hass):
devices_response=[DEVICE_1, DEVICE_2], devices_response=[DEVICE_1, DEVICE_2],
known_wireless_clients=(CLIENT_4["mac"],), known_wireless_clients=(CLIENT_4["mac"],),
) )
assert len(hass.states.async_all()) == 7 assert len(hass.states.async_all()) == 6
client_1 = hass.states.get("device_tracker.client_1") client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None assert client_1 is not None
@@ -184,7 +184,7 @@ async def test_controller_state_change(hass):
controller = await setup_unifi_integration( controller = await setup_unifi_integration(
hass, clients_response=[CLIENT_1], devices_response=[DEVICE_1], hass, clients_response=[CLIENT_1], devices_response=[DEVICE_1],
) )
assert len(hass.states.async_all()) == 3 assert len(hass.states.async_all()) == 2
# Controller unavailable # Controller unavailable
controller.async_unifi_signalling_callback( controller.async_unifi_signalling_callback(
@@ -214,7 +214,7 @@ async def test_option_track_clients(hass):
controller = await setup_unifi_integration( controller = await setup_unifi_integration(
hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1],
) )
assert len(hass.states.async_all()) == 4 assert len(hass.states.async_all()) == 3
client_1 = hass.states.get("device_tracker.client_1") client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None assert client_1 is not None
@@ -259,7 +259,7 @@ async def test_option_track_wired_clients(hass):
controller = await setup_unifi_integration( controller = await setup_unifi_integration(
hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1],
) )
assert len(hass.states.async_all()) == 4 assert len(hass.states.async_all()) == 3
client_1 = hass.states.get("device_tracker.client_1") client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None assert client_1 is not None
@@ -304,7 +304,7 @@ async def test_option_track_devices(hass):
controller = await setup_unifi_integration( controller = await setup_unifi_integration(
hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1],
) )
assert len(hass.states.async_all()) == 4 assert len(hass.states.async_all()) == 3
client_1 = hass.states.get("device_tracker.client_1") client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None assert client_1 is not None
@@ -349,7 +349,7 @@ async def test_option_ssid_filter(hass):
controller = await setup_unifi_integration( controller = await setup_unifi_integration(
hass, options={CONF_SSID_FILTER: ["ssid"]}, clients_response=[CLIENT_3], hass, options={CONF_SSID_FILTER: ["ssid"]}, clients_response=[CLIENT_3],
) )
assert len(hass.states.async_all()) == 2 assert len(hass.states.async_all()) == 1
# SSID filter active # SSID filter active
client_3 = hass.states.get("device_tracker.client_3") client_3 = hass.states.get("device_tracker.client_3")
@@ -387,7 +387,7 @@ async def test_wireless_client_go_wired_issue(hass):
client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
controller = await setup_unifi_integration(hass, clients_response=[client_1_client]) controller = await setup_unifi_integration(hass, clients_response=[client_1_client])
assert len(hass.states.async_all()) == 2 assert len(hass.states.async_all()) == 1
client_1 = hass.states.get("device_tracker.client_1") client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None assert client_1 is not None
@@ -460,7 +460,7 @@ async def test_restoring_client(hass):
clients_response=[CLIENT_2], clients_response=[CLIENT_2],
clients_all_response=[CLIENT_1], clients_all_response=[CLIENT_1],
) )
assert len(hass.states.async_all()) == 3 assert len(hass.states.async_all()) == 2
device_1 = hass.states.get("device_tracker.client_1") device_1 = hass.states.get("device_tracker.client_1")
assert device_1 is not None assert device_1 is not None
@@ -474,7 +474,7 @@ async def test_dont_track_clients(hass):
clients_response=[CLIENT_1], clients_response=[CLIENT_1],
devices_response=[DEVICE_1], devices_response=[DEVICE_1],
) )
assert len(hass.states.async_all()) == 2 assert len(hass.states.async_all()) == 1
client_1 = hass.states.get("device_tracker.client_1") client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is None assert client_1 is None
@@ -492,7 +492,7 @@ async def test_dont_track_devices(hass):
clients_response=[CLIENT_1], clients_response=[CLIENT_1],
devices_response=[DEVICE_1], devices_response=[DEVICE_1],
) )
assert len(hass.states.async_all()) == 2 assert len(hass.states.async_all()) == 1
client_1 = hass.states.get("device_tracker.client_1") client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None assert client_1 is not None
@@ -509,7 +509,7 @@ async def test_dont_track_wired_clients(hass):
options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False}, options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False},
clients_response=[CLIENT_1, CLIENT_2], clients_response=[CLIENT_1, CLIENT_2],
) )
assert len(hass.states.async_all()) == 2 assert len(hass.states.async_all()) == 1
client_1 = hass.states.get("device_tracker.client_1") client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None assert client_1 is not None

View File

@@ -55,7 +55,7 @@ async def test_no_clients(hass):
) )
assert len(controller.mock_requests) == 4 assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 0
async def test_sensors(hass): async def test_sensors(hass):
@@ -71,7 +71,7 @@ async def test_sensors(hass):
) )
assert len(controller.mock_requests) == 4 assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 5 assert len(hass.states.async_all()) == 4
wired_client_rx = hass.states.get("sensor.wired_client_name_rx") wired_client_rx = hass.states.get("sensor.wired_client_name_rx")
assert wired_client_rx.state == "1234.0" assert wired_client_rx.state == "1234.0"

View File

@@ -209,7 +209,7 @@ async def test_no_clients(hass):
) )
assert len(controller.mock_requests) == 4 assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 0
async def test_controller_not_client(hass): async def test_controller_not_client(hass):
@@ -222,7 +222,7 @@ async def test_controller_not_client(hass):
) )
assert len(controller.mock_requests) == 4 assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 0
cloudkey = hass.states.get("switch.cloud_key") cloudkey = hass.states.get("switch.cloud_key")
assert cloudkey is None assert cloudkey is None
@@ -240,7 +240,7 @@ async def test_not_admin(hass):
) )
assert len(controller.mock_requests) == 4 assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 0
async def test_switches(hass): async def test_switches(hass):
@@ -258,7 +258,7 @@ async def test_switches(hass):
) )
assert len(controller.mock_requests) == 4 assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 4 assert len(hass.states.async_all()) == 3
switch_1 = hass.states.get("switch.poe_client_1") switch_1 = hass.states.get("switch.poe_client_1")
assert switch_1 is not None assert switch_1 is not None
@@ -312,7 +312,7 @@ async def test_new_client_discovered_on_block_control(hass):
) )
assert len(controller.mock_requests) == 4 assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 0
blocked = hass.states.get("switch.block_client_1") blocked = hass.states.get("switch.block_client_1")
assert blocked is None assert blocked is None
@@ -324,7 +324,7 @@ async def test_new_client_discovered_on_block_control(hass):
controller.api.session_handler("data") controller.api.session_handler("data")
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_all()) == 2 assert len(hass.states.async_all()) == 1
blocked = hass.states.get("switch.block_client_1") blocked = hass.states.get("switch.block_client_1")
assert blocked is not None assert blocked is not None
@@ -336,7 +336,7 @@ async def test_option_block_clients(hass):
options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]},
clients_all_response=[BLOCKED, UNBLOCKED], clients_all_response=[BLOCKED, UNBLOCKED],
) )
assert len(hass.states.async_all()) == 2 assert len(hass.states.async_all()) == 1
# Add a second switch # Add a second switch
hass.config_entries.async_update_entry( hass.config_entries.async_update_entry(
@@ -344,28 +344,28 @@ async def test_option_block_clients(hass):
options={CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]}, options={CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_all()) == 3 assert len(hass.states.async_all()) == 2
# Remove the second switch again # Remove the second switch again
hass.config_entries.async_update_entry( hass.config_entries.async_update_entry(
controller.config_entry, options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, controller.config_entry, options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_all()) == 2 assert len(hass.states.async_all()) == 1
# Enable one and remove another one # Enable one and remove another one
hass.config_entries.async_update_entry( hass.config_entries.async_update_entry(
controller.config_entry, options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}, controller.config_entry, options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_all()) == 2 assert len(hass.states.async_all()) == 1
# Remove one # Remove one
hass.config_entries.async_update_entry( hass.config_entries.async_update_entry(
controller.config_entry, options={CONF_BLOCK_CLIENT: []}, controller.config_entry, options={CONF_BLOCK_CLIENT: []},
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1 assert len(hass.states.async_all()) == 0
async def test_new_client_discovered_on_poe_control(hass): async def test_new_client_discovered_on_poe_control(hass):
@@ -378,7 +378,7 @@ async def test_new_client_discovered_on_poe_control(hass):
) )
assert len(controller.mock_requests) == 4 assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 2 assert len(hass.states.async_all()) == 1
controller.api.websocket._data = { controller.api.websocket._data = {
"meta": {"message": "sta:sync"}, "meta": {"message": "sta:sync"},
@@ -391,7 +391,7 @@ async def test_new_client_discovered_on_poe_control(hass):
"switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True "switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True
) )
assert len(controller.mock_requests) == 5 assert len(controller.mock_requests) == 5
assert len(hass.states.async_all()) == 3 assert len(hass.states.async_all()) == 2
assert controller.mock_requests[4] == { assert controller.mock_requests[4] == {
"json": { "json": {
"port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}] "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}]
@@ -430,7 +430,7 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass):
) )
assert len(controller.mock_requests) == 4 assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 4 assert len(hass.states.async_all()) == 3
switch_1 = hass.states.get("switch.poe_client_1") switch_1 = hass.states.get("switch.poe_client_1")
switch_2 = hass.states.get("switch.poe_client_2") switch_2 = hass.states.get("switch.poe_client_2")
@@ -481,7 +481,7 @@ async def test_restoring_client(hass):
) )
assert len(controller.mock_requests) == 4 assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 3 assert len(hass.states.async_all()) == 2
device_1 = hass.states.get("switch.client_1") device_1 = hass.states.get("switch.client_1")
assert device_1 is not None assert device_1 is not None

View File

@@ -102,6 +102,23 @@ def make_attribute(attrid, value, status=0):
return attr return attr
def send_attribute_report(hass, cluster, attrid, value):
"""Send a single attribute report."""
return send_attributes_report(hass, cluster, {attrid: value})
async def send_attributes_report(hass, cluster: int, attributes: dict):
"""Cause the sensor to receive an attribute report from the network.
This is to simulate the normal device communication that happens when a
device is paired to the zigbee network.
"""
attrs = [make_attribute(attrid, value) for attrid, value in attributes.items()]
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [attrs])
await hass.async_block_till_done()
async def find_entity_id(domain, zha_device, hass): async def find_entity_id(domain, zha_device, hass):
"""Find the entity id under the testing. """Find the entity id under the testing.

View File

@@ -2,7 +2,6 @@
import pytest import pytest
import zigpy.zcl.clusters.measurement as measurement import zigpy.zcl.clusters.measurement as measurement
import zigpy.zcl.clusters.security as security import zigpy.zcl.clusters.security as security
import zigpy.zcl.foundation as zcl_f
from homeassistant.components.binary_sensor import DOMAIN from homeassistant.components.binary_sensor import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
@@ -11,8 +10,7 @@ from .common import (
async_enable_traffic, async_enable_traffic,
async_test_rejoin, async_test_rejoin,
find_entity_id, find_entity_id,
make_attribute, send_attributes_report,
make_zcl_header,
) )
DEVICE_IAS = { DEVICE_IAS = {
@@ -36,17 +34,11 @@ DEVICE_OCCUPANCY = {
async def async_test_binary_sensor_on_off(hass, cluster, entity_id): async def async_test_binary_sensor_on_off(hass, cluster, entity_id):
"""Test getting on and off messages for binary sensors.""" """Test getting on and off messages for binary sensors."""
# binary sensor on # binary sensor on
attr = make_attribute(0, 1) await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 2})
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(entity_id).state == STATE_ON
# binary sensor off # binary sensor off
attr.value.value = 0 await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2})
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF

View File

@@ -14,8 +14,7 @@ from .common import (
async_enable_traffic, async_enable_traffic,
async_test_rejoin, async_test_rejoin,
find_entity_id, find_entity_id,
make_attribute, send_attributes_report,
make_zcl_header,
) )
from tests.common import mock_coro from tests.common import mock_coro
@@ -64,19 +63,12 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device):
await async_enable_traffic(hass, [zha_device]) await async_enable_traffic(hass, [zha_device])
await hass.async_block_till_done() await hass.async_block_till_done()
attr = make_attribute(8, 100)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
# test that the state has changed from unavailable to off # test that the state has changed from unavailable to off
await send_attributes_report(hass, cluster, {0: 0, 8: 100, 1: 1})
assert hass.states.get(entity_id).state == STATE_CLOSED assert hass.states.get(entity_id).state == STATE_CLOSED
# test to see if it opens # test to see if it opens
attr = make_attribute(8, 0) await send_attributes_report(hass, cluster, {0: 1, 8: 0, 1: 100})
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OPEN assert hass.states.get(entity_id).state == STATE_OPEN
# close from UI # close from UI

View File

@@ -4,7 +4,6 @@ import time
import pytest import pytest
import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.general as general
import zigpy.zcl.foundation as zcl_f
from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER
from homeassistant.components.zha.core.registries import ( from homeassistant.components.zha.core.registries import (
@@ -17,8 +16,7 @@ from .common import (
async_enable_traffic, async_enable_traffic,
async_test_rejoin, async_test_rejoin,
find_entity_id, find_entity_id,
make_attribute, send_attributes_report,
make_zcl_header,
) )
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
@@ -66,12 +64,9 @@ async def test_device_tracker(hass, zha_device_joined_restored, zigpy_device_dt)
assert hass.states.get(entity_id).state == STATE_NOT_HOME assert hass.states.get(entity_id).state == STATE_NOT_HOME
# turn state flip # turn state flip
attr = make_attribute(0x0020, 23) await send_attributes_report(
hdr = make_zcl_header(zcl_f.Command.Report_Attributes) hass, cluster, {0x0000: 0, 0x0020: 23, 0x0021: 200, 0x0001: 2}
cluster.handle_message(hdr, [[attr]]) )
attr = make_attribute(0x0021, 200)
cluster.handle_message(hdr, [[attr]])
zigpy_device_dt.last_seen = time.time() + 10 zigpy_device_dt.last_seen = time.time() + 10
next_update = dt_util.utcnow() + timedelta(seconds=30) next_update = dt_util.utcnow() + timedelta(seconds=30)

View File

@@ -3,7 +3,6 @@ from unittest.mock import call
import pytest import pytest
import zigpy.zcl.clusters.hvac as hvac import zigpy.zcl.clusters.hvac as hvac
import zigpy.zcl.foundation as zcl_f
from homeassistant.components import fan from homeassistant.components import fan
from homeassistant.components.fan import ATTR_SPEED, DOMAIN, SERVICE_SET_SPEED from homeassistant.components.fan import ATTR_SPEED, DOMAIN, SERVICE_SET_SPEED
@@ -20,8 +19,7 @@ from .common import (
async_enable_traffic, async_enable_traffic,
async_test_rejoin, async_test_rejoin,
find_entity_id, find_entity_id,
make_attribute, send_attributes_report,
make_zcl_header,
) )
@@ -52,16 +50,11 @@ async def test_fan(hass, zha_device_joined_restored, zigpy_device):
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
# turn on at fan # turn on at fan
attr = make_attribute(0, 1) await send_attributes_report(hass, cluster, {1: 2, 0: 1, 2: 3})
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(entity_id).state == STATE_ON
# turn off at fan # turn off at fan
attr.value.value = 0 await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2})
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
# turn on from HA # turn on from HA

View File

@@ -19,8 +19,7 @@ from .common import (
async_enable_traffic, async_enable_traffic,
async_test_rejoin, async_test_rejoin,
find_entity_id, find_entity_id,
make_attribute, send_attributes_report,
make_zcl_header,
) )
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
@@ -190,26 +189,18 @@ async def test_light(
async def async_test_on_off_from_light(hass, cluster, entity_id): async def async_test_on_off_from_light(hass, cluster, entity_id):
"""Test on off functionality from the light.""" """Test on off functionality from the light."""
# turn on at light # turn on at light
attr = make_attribute(0, 1) await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 3})
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(entity_id).state == STATE_ON
# turn off at light # turn off at light
attr.value.value = 0 await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 3})
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
async def async_test_on_from_light(hass, cluster, entity_id): async def async_test_on_from_light(hass, cluster, entity_id):
"""Test on off functionality from the light.""" """Test on off functionality from the light."""
# turn on at light # turn on at light
attr = make_attribute(0, 1) await send_attributes_report(hass, cluster, {1: -1, 0: 1, 2: 2})
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(entity_id).state == STATE_ON
@@ -316,10 +307,10 @@ async def async_test_level_on_off_from_hass(
async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state):
"""Test dimmer functionality from the light.""" """Test dimmer functionality from the light."""
attr = make_attribute(0, level)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes) await send_attributes_report(
cluster.handle_message(hdr, [[attr]]) hass, cluster, {1: level + 10, 0: level, 2: level - 10 or 22}
await hass.async_block_till_done() )
assert hass.states.get(entity_id).state == expected_state assert hass.states.get(entity_id).state == expected_state
# hass uses None for brightness of 0 in state attributes # hass uses None for brightness of 0 in state attributes
if level == 0: if level == 0:

View File

@@ -10,12 +10,7 @@ import zigpy.zcl.foundation as zcl_f
from homeassistant.components.lock import DOMAIN from homeassistant.components.lock import DOMAIN
from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED
from .common import ( from .common import async_enable_traffic, find_entity_id, send_attributes_report
async_enable_traffic,
find_entity_id,
make_attribute,
make_zcl_header,
)
from tests.common import mock_coro from tests.common import mock_coro
@@ -58,16 +53,11 @@ async def test_lock(hass, lock):
assert hass.states.get(entity_id).state == STATE_UNLOCKED assert hass.states.get(entity_id).state == STATE_UNLOCKED
# set state to locked # set state to locked
attr = make_attribute(0, 1) await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 2})
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_LOCKED assert hass.states.get(entity_id).state == STATE_LOCKED
# set state to unlocked # set state to unlocked
attr.value.value = 2 await send_attributes_report(hass, cluster, {1: 0, 0: 2, 2: 3})
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_UNLOCKED assert hass.states.get(entity_id).state == STATE_UNLOCKED
# lock from HA # lock from HA

View File

@@ -6,7 +6,6 @@ import zigpy.zcl.clusters.general as general
import zigpy.zcl.clusters.homeautomation as homeautomation import zigpy.zcl.clusters.homeautomation as homeautomation
import zigpy.zcl.clusters.measurement as measurement import zigpy.zcl.clusters.measurement as measurement
import zigpy.zcl.clusters.smartenergy as smartenergy import zigpy.zcl.clusters.smartenergy as smartenergy
import zigpy.zcl.foundation as zcl_f
from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor import DOMAIN
import homeassistant.config as config_util import homeassistant.config as config_util
@@ -28,38 +27,41 @@ from .common import (
async_enable_traffic, async_enable_traffic,
async_test_rejoin, async_test_rejoin,
find_entity_id, find_entity_id,
make_attribute, send_attribute_report,
make_zcl_header, send_attributes_report,
) )
async def async_test_humidity(hass, cluster, entity_id): async def async_test_humidity(hass, cluster, entity_id):
"""Test humidity sensor.""" """Test humidity sensor."""
await send_attribute_report(hass, cluster, 0, 1000) await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 100})
assert_state(hass, entity_id, "10.0", UNIT_PERCENTAGE) assert_state(hass, entity_id, "10.0", UNIT_PERCENTAGE)
async def async_test_temperature(hass, cluster, entity_id): async def async_test_temperature(hass, cluster, entity_id):
"""Test temperature sensor.""" """Test temperature sensor."""
await send_attribute_report(hass, cluster, 0, 2900) await send_attributes_report(hass, cluster, {1: 1, 0: 2900, 2: 100})
assert_state(hass, entity_id, "29.0", "°C") assert_state(hass, entity_id, "29.0", "°C")
async def async_test_pressure(hass, cluster, entity_id): async def async_test_pressure(hass, cluster, entity_id):
"""Test pressure sensor.""" """Test pressure sensor."""
await send_attribute_report(hass, cluster, 0, 1000) await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 10000})
assert_state(hass, entity_id, "1000", "hPa")
await send_attributes_report(hass, cluster, {0: 1000, 20: -1, 16: 10000})
assert_state(hass, entity_id, "1000", "hPa") assert_state(hass, entity_id, "1000", "hPa")
async def async_test_illuminance(hass, cluster, entity_id): async def async_test_illuminance(hass, cluster, entity_id):
"""Test illuminance sensor.""" """Test illuminance sensor."""
await send_attribute_report(hass, cluster, 0, 10) await send_attributes_report(hass, cluster, {1: 1, 0: 10, 2: 20})
assert_state(hass, entity_id, "1.0", "lx") assert_state(hass, entity_id, "1.0", "lx")
async def async_test_metering(hass, cluster, entity_id): async def async_test_metering(hass, cluster, entity_id):
"""Test metering sensor.""" """Test metering sensor."""
await send_attribute_report(hass, cluster, 1024, 12345) await send_attributes_report(hass, cluster, {1025: 1, 1024: 12345, 1026: 100})
assert_state(hass, entity_id, "12345.0", "unknown") assert_state(hass, entity_id, "12345.0", "unknown")
@@ -73,17 +75,17 @@ async def async_test_electrical_measurement(hass, cluster, entity_id):
new_callable=mock.PropertyMock, new_callable=mock.PropertyMock,
) as divisor_mock: ) as divisor_mock:
divisor_mock.return_value = 1 divisor_mock.return_value = 1
await send_attribute_report(hass, cluster, 1291, 100) await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000})
assert_state(hass, entity_id, "100", "W") assert_state(hass, entity_id, "100", "W")
await send_attribute_report(hass, cluster, 1291, 99) await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 1000})
assert_state(hass, entity_id, "99", "W") assert_state(hass, entity_id, "99", "W")
divisor_mock.return_value = 10 divisor_mock.return_value = 10
await send_attribute_report(hass, cluster, 1291, 1000) await send_attributes_report(hass, cluster, {0: 1, 1291: 1000, 10: 5000})
assert_state(hass, entity_id, "100", "W") assert_state(hass, entity_id, "100", "W")
await send_attribute_report(hass, cluster, 1291, 99) await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 5000})
assert_state(hass, entity_id, "9.9", "W") assert_state(hass, entity_id, "9.9", "W")
@@ -141,18 +143,6 @@ async def test_sensor(
await async_test_rejoin(hass, zigpy_device, [cluster], (report_count,)) await async_test_rejoin(hass, zigpy_device, [cluster], (report_count,))
async def send_attribute_report(hass, cluster, attrid, value):
"""Cause the sensor to receive an attribute report from the network.
This is to simulate the normal device communication that happens when a
device is paired to the zigbee network.
"""
attr = make_attribute(attrid, value)
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
def assert_state(hass, entity_id, state, unit_of_measurement): def assert_state(hass, entity_id, state, unit_of_measurement):
"""Check that the state is what is expected. """Check that the state is what is expected.

View File

@@ -12,8 +12,7 @@ from .common import (
async_enable_traffic, async_enable_traffic,
async_test_rejoin, async_test_rejoin,
find_entity_id, find_entity_id,
make_attribute, send_attributes_report,
make_zcl_header,
) )
from tests.common import mock_coro from tests.common import mock_coro
@@ -53,16 +52,11 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device):
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
# turn on at switch # turn on at switch
attr = make_attribute(0, 1) await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 2})
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON assert hass.states.get(entity_id).state == STATE_ON
# turn off at switch # turn off at switch
attr.value.value = 0 await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2})
cluster.handle_message(hdr, [[attr]])
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
# turn on from HA # turn on from HA