Compare commits

...

16 Commits

Author SHA1 Message Date
Paulus Schoutsen
111a00aeeb Bumped version to 0.111.0b4 2020-06-08 12:28:53 -07:00
Franck Nijhof
b3d5717df8 Fix mobile_app sensor re-registration handling (#36567) 2020-06-08 12:27:12 -07:00
Paulus Schoutsen
a84378bb79 Mobile app fixes (#36559) 2020-06-08 12:27:11 -07:00
shbatm
53ba45cc8f Add Z-Wave Notification Sensor support to ISY994 (#36548) 2020-06-08 12:27:10 -07:00
Jörg Thalheim
5d77eb1839 Fix intent component initialisation (#36064)
The intent component expect this method from every module that is called intent.
Fixes #35522
2020-06-08 12:27:09 -07:00
Paulus Schoutsen
517159dc4e Merge remote-tracking branch 'origin/master' into rc 2020-06-08 11:17:27 -07:00
Paulus Schoutsen
a9287b7117 Merge pull request #36579 from home-assistant/110.6 2020-06-08 11:15:35 -07:00
Paulus Schoutsen
0b7bcc87df Bumped version to 0.110.6 2020-06-08 10:14:45 -07:00
Franck Nijhof
482661f82c Fix mobile_app registering/update sensor values with an unknown state (#36566) 2020-06-08 10:14:40 -07:00
Paulus Schoutsen
865d65c380 Bumped version to 0.111.0b3 2020-06-07 17:24:48 -07:00
Franck Nijhof
609d202c4d Fix WLED power and brightness with WLED 0.10+ (#36529) 2020-06-07 17:24:43 -07:00
matgad
e43a0087e4 Bump version zigpy-cc (#36506) 2020-06-07 17:24:43 -07:00
Paulus Schoutsen
b9bc147339 Update netdisco (#36499) 2020-06-07 17:24:42 -07:00
Bram Kragten
dd3b0df22d Update frontend to 20200603.2 (#36494) 2020-06-07 17:24:41 -07:00
Erik Montnemery
c987ca735e Bump pychromecast to 6.0.0 (#36414)
* Revert "Prevent race in pychromecast.start_discovery (#36350)"

This reverts commit 391983a0cf.

* Adapt to pychromecast 6.0.0
2020-06-07 17:24:40 -07:00
Paulus Schoutsen
4b1761ccb5 Use builtin mock (#36473) 2020-06-05 15:12:50 -07:00
25 changed files with 537 additions and 169 deletions

View File

@@ -40,6 +40,16 @@ def async_setup(hass):
hass.http.register_view(AlexaIntentsView) hass.http.register_view(AlexaIntentsView)
async def async_setup_intents(hass):
"""
Do intents setup.
Right now this module does not expose any, but the intent component breaks
without it.
"""
pass # pylint: disable=unnecessary-pass
class UnknownRequest(HomeAssistantError): class UnknownRequest(HomeAssistantError):
"""When an unknown Alexa request is passed in.""" """When an unknown Alexa request is passed in."""

View File

@@ -3,7 +3,6 @@ import logging
import threading import threading
import pychromecast import pychromecast
import zeroconf
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -85,14 +84,12 @@ def setup_internal_discovery(hass: HomeAssistant) -> None:
) )
_LOGGER.debug("Starting internal pychromecast discovery.") _LOGGER.debug("Starting internal pychromecast discovery.")
listener = pychromecast.discovery.CastListener( listener = pychromecast.CastListener(
internal_add_callback, internal_remove_callback internal_add_callback,
) internal_remove_callback,
browser = zeroconf.ServiceBrowser( internal_add_callback, # Use internal_add_callback also for updates
ChromeCastZeroconf.get_zeroconf() or zeroconf.Zeroconf(),
"_googlecast._tcp.local.",
listener,
) )
browser = pychromecast.start_discovery(listener, ChromeCastZeroconf.get_zeroconf())
def stop_discovery(event): def stop_discovery(event):
"""Stop discovery of new chromecasts.""" """Stop discovery of new chromecasts."""

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==5.3.0"], "requirements": ["pychromecast==6.0.0"],
"after_dependencies": ["cloud","zeroconf"], "after_dependencies": ["cloud","zeroconf"],
"zeroconf": ["_googlecast._tcp.local."], "zeroconf": ["_googlecast._tcp.local."],
"codeowners": ["@emontnemery"] "codeowners": ["@emontnemery"]

View File

@@ -2,7 +2,7 @@
"domain": "discovery", "domain": "discovery",
"name": "Discovery", "name": "Discovery",
"documentation": "https://www.home-assistant.io/integrations/discovery", "documentation": "https://www.home-assistant.io/integrations/discovery",
"requirements": ["netdisco==2.6.0"], "requirements": ["netdisco==2.7.0"],
"after_dependencies": ["zeroconf"], "after_dependencies": ["zeroconf"],
"codeowners": [], "codeowners": [],
"quality_scale": "internal" "quality_scale": "internal"

View File

@@ -2,7 +2,7 @@
"domain": "frontend", "domain": "frontend",
"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": ["home-assistant-frontend==20200603.1"], "requirements": ["home-assistant-frontend==20200603.2"],
"dependencies": [ "dependencies": [
"api", "api",
"auth", "auth",

View File

@@ -232,7 +232,7 @@ NODE_FILTERS = {
"RemoteLinc2_ADV", "RemoteLinc2_ADV",
], ],
FILTER_INSTEON_TYPE: ["0.16.", "0.17.", "0.18.", "9.0.", "9.7."], FILTER_INSTEON_TYPE: ["0.16.", "0.17.", "0.18.", "9.0.", "9.7."],
FILTER_ZWAVE_CAT: (["118", "143"] + list(map(str, range(180, 185)))), FILTER_ZWAVE_CAT: (["118", "143"] + list(map(str, range(180, 186)))),
}, },
LOCK: { LOCK: {
FILTER_UOM: ["11"], FILTER_UOM: ["11"],

View File

@@ -56,7 +56,6 @@ ERR_ENCRYPTION_ALREADY_ENABLED = "encryption_already_enabled"
ERR_ENCRYPTION_NOT_AVAILABLE = "encryption_not_available" ERR_ENCRYPTION_NOT_AVAILABLE = "encryption_not_available"
ERR_ENCRYPTION_REQUIRED = "encryption_required" ERR_ENCRYPTION_REQUIRED = "encryption_required"
ERR_SENSOR_NOT_REGISTERED = "not_registered" ERR_SENSOR_NOT_REGISTERED = "not_registered"
ERR_SENSOR_DUPLICATE_UNIQUE_ID = "duplicate_unique_id"
ERR_INVALID_FORMAT = "invalid_format" ERR_INVALID_FORMAT = "invalid_format"

View File

@@ -1,4 +1,5 @@
"""Webhook handlers for mobile_app.""" """Webhook handlers for mobile_app."""
import asyncio
from functools import wraps from functools import wraps
import logging import logging
import secrets import secrets
@@ -28,7 +29,7 @@ from homeassistant.const import (
HTTP_CREATED, HTTP_CREATED,
) )
from homeassistant.core import EventOrigin from homeassistant.core import EventOrigin
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError from homeassistant.exceptions import ServiceNotFound, TemplateError
from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.template import attach from homeassistant.helpers.template import attach
@@ -77,7 +78,6 @@ from .const import (
ERR_ENCRYPTION_NOT_AVAILABLE, ERR_ENCRYPTION_NOT_AVAILABLE,
ERR_ENCRYPTION_REQUIRED, ERR_ENCRYPTION_REQUIRED,
ERR_INVALID_FORMAT, ERR_INVALID_FORMAT,
ERR_SENSOR_DUPLICATE_UNIQUE_ID,
ERR_SENSOR_NOT_REGISTERED, ERR_SENSOR_NOT_REGISTERED,
SIGNAL_LOCATION_UPDATE, SIGNAL_LOCATION_UPDATE,
SIGNAL_SENSOR_UPDATE, SIGNAL_SENSOR_UPDATE,
@@ -95,6 +95,7 @@ from .helpers import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DELAY_SAVE = 10
WEBHOOK_COMMANDS = Registry() WEBHOOK_COMMANDS = Registry()
@@ -184,7 +185,10 @@ async def handle_webhook(
"Received webhook payload for type %s: %s", webhook_type, webhook_payload "Received webhook payload for type %s: %s", webhook_type, webhook_payload
) )
return await WEBHOOK_COMMANDS[webhook_type](hass, config_entry, webhook_payload) # Shield so we make sure we finish the webhook, even if sender hangs up.
return await asyncio.shield(
WEBHOOK_COMMANDS[webhook_type](hass, config_entry, webhook_payload)
)
@WEBHOOK_COMMANDS.register("call_service") @WEBHOOK_COMMANDS.register("call_service")
@@ -352,38 +356,37 @@ async def webhook_enable_encryption(hass, config_entry, data):
vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES),
vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string,
vol.Optional(ATTR_SENSOR_UOM): cv.string, vol.Optional(ATTR_SENSOR_UOM): cv.string,
vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, str, int, float),
vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon,
} }
) )
async def webhook_register_sensor(hass, config_entry, data): async def webhook_register_sensor(hass, config_entry, data):
"""Handle a register sensor webhook.""" """Handle a register sensor webhook."""
entity_type = data[ATTR_SENSOR_TYPE] entity_type = data[ATTR_SENSOR_TYPE]
unique_id = data[ATTR_SENSOR_UNIQUE_ID] unique_id = data[ATTR_SENSOR_UNIQUE_ID]
unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}"
existing_sensor = unique_store_key in hass.data[DOMAIN][entity_type]
if unique_store_key in hass.data[DOMAIN][entity_type]:
_LOGGER.error("Refusing to re-register existing sensor %s!", unique_id)
return error_response(
ERR_SENSOR_DUPLICATE_UNIQUE_ID,
f"{entity_type} {unique_id} already exists!",
status=409,
)
data[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] data[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID]
# If sensor already is registered, update current state instead
if existing_sensor:
_LOGGER.debug("Re-register existing sensor %s", unique_id)
entry = hass.data[DOMAIN][entity_type][unique_store_key]
data = {**entry, **data}
hass.data[DOMAIN][entity_type][unique_store_key] = data hass.data[DOMAIN][entity_type][unique_store_key] = data
try: hass.data[DOMAIN][DATA_STORE].async_delay_save(
await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass)) lambda: savable_state(hass), DELAY_SAVE
except HomeAssistantError as ex: )
_LOGGER.error("Error registering sensor: %s", ex)
return empty_okay_response()
register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register" if existing_sensor:
async_dispatcher_send(hass, register_signal, data) async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, data)
else:
register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register"
async_dispatcher_send(hass, register_signal, data)
return webhook_response( return webhook_response(
{"success": True}, registration=config_entry.data, status=HTTP_CREATED, {"success": True}, registration=config_entry.data, status=HTTP_CREATED,
@@ -414,7 +417,7 @@ async def webhook_update_sensor_states(hass, config_entry, data):
{ {
vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict,
vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon,
vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, str, int, float),
vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES),
vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string,
} }
@@ -458,18 +461,14 @@ async def webhook_update_sensor_states(hass, config_entry, data):
hass.data[DOMAIN][entity_type][unique_store_key] = new_state hass.data[DOMAIN][entity_type][unique_store_key] = new_state
safe = savable_state(hass)
try:
await hass.data[DOMAIN][DATA_STORE].async_save(safe)
except HomeAssistantError as ex:
_LOGGER.error("Error updating mobile_app registration: %s", ex)
return empty_okay_response()
async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state)
resp[unique_id] = {"success": True} resp[unique_id] = {"success": True}
hass.data[DOMAIN][DATA_STORE].async_delay_save(
lambda: savable_state(hass), DELAY_SAVE
)
return webhook_response(resp, registration=config_entry.data) return webhook_response(resp, registration=config_entry.data)

View File

@@ -2,7 +2,7 @@
"domain": "ssdp", "domain": "ssdp",
"name": "Simple Service Discovery Protocol (SSDP)", "name": "Simple Service Discovery Protocol (SSDP)",
"documentation": "https://www.home-assistant.io/integrations/ssdp", "documentation": "https://www.home-assistant.io/integrations/ssdp",
"requirements": ["defusedxml==0.6.0", "netdisco==2.6.0"], "requirements": ["defusedxml==0.6.0", "netdisco==2.7.0"],
"after_dependencies": ["zeroconf"], "after_dependencies": ["zeroconf"],
"codeowners": [] "codeowners": []
} }

View File

@@ -83,20 +83,78 @@ async def async_setup_entry(
update_segments() update_segments()
class WLEDLight(LightEntity, WLEDDeviceEntity): class WLEDMasterLight(LightEntity, WLEDDeviceEntity):
"""Defines a WLED light.""" """Defines a WLED master light."""
def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator):
"""Initialize WLED master light."""
super().__init__(
entry_id=entry_id,
coordinator=coordinator,
name=f"{coordinator.data.info.name} Master",
icon="mdi:led-strip-variant",
)
@property
def unique_id(self) -> str:
"""Return the unique ID for this sensor."""
return f"{self.coordinator.data.info.mac_address}"
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
@property
def brightness(self) -> Optional[int]:
"""Return the brightness of this light between 1..255."""
return self.coordinator.data.state.brightness
@property
def is_on(self) -> bool:
"""Return the state of the light."""
return bool(self.coordinator.data.state.on)
@wled_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
data = {ATTR_ON: False}
if ATTR_TRANSITION in kwargs:
# WLED uses 100ms per unit, so 10 = 1 second.
data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10)
await self.coordinator.wled.master(**data)
@wled_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
data = {ATTR_ON: True}
if ATTR_TRANSITION in kwargs:
# WLED uses 100ms per unit, so 10 = 1 second.
data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10)
if ATTR_BRIGHTNESS in kwargs:
data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS]
await self.coordinator.wled.master(**data)
class WLEDSegmentLight(LightEntity, WLEDDeviceEntity):
"""Defines a WLED light based on a segment."""
def __init__( def __init__(
self, entry_id: str, coordinator: WLEDDataUpdateCoordinator, segment: int self, entry_id: str, coordinator: WLEDDataUpdateCoordinator, segment: int
): ):
"""Initialize WLED light.""" """Initialize WLED segment light."""
self._rgbw = coordinator.data.info.leds.rgbw self._rgbw = coordinator.data.info.leds.rgbw
self._segment = segment self._segment = segment
# Only apply the segment ID if it is not the first segment # If this is the one and only segment, use a simpler name
name = coordinator.data.info.name name = f"{coordinator.data.info.name} Segment {self._segment}"
if segment != 0: if len(coordinator.data.state.segments) == 1:
name += f" {segment}" name = coordinator.data.info.name
super().__init__( super().__init__(
entry_id=entry_id, entry_id=entry_id,
@@ -155,7 +213,16 @@ class WLEDLight(LightEntity, WLEDDeviceEntity):
@property @property
def brightness(self) -> Optional[int]: def brightness(self) -> Optional[int]:
"""Return the brightness of this light between 1..255.""" """Return the brightness of this light between 1..255."""
return self.coordinator.data.state.brightness state = self.coordinator.data.state
# If this is the one and only segment, calculate brightness based
# on the master and segment brightness
if len(state.segments) == 1:
return int(
(state.segments[self._segment].brightness * state.brightness) / 255
)
return state.segments[self._segment].brightness
@property @property
def white_value(self) -> Optional[int]: def white_value(self) -> Optional[int]:
@@ -187,18 +254,30 @@ class WLEDLight(LightEntity, WLEDDeviceEntity):
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the state of the light.""" """Return the state of the light."""
return bool(self.coordinator.data.state.on) state = self.coordinator.data.state
# If there is a single segment, take master into account
if len(state.segments) == 1 and not state.on:
return False
return bool(state.segments[self._segment].on)
@wled_exception_handler @wled_exception_handler
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light.""" """Turn off the light."""
data = {ATTR_ON: False, ATTR_SEGMENT_ID: self._segment} data = {ATTR_ON: False}
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
# WLED uses 100ms per unit, so 10 = 1 second. # WLED uses 100ms per unit, so 10 = 1 second.
data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10)
await self.coordinator.wled.light(**data) # If there is a single segment, control via the master
if len(self.coordinator.data.state.segments) == 1:
await self.coordinator.wled.master(**data)
return
data[ATTR_SEGMENT_ID] = self._segment
await self.coordinator.wled.segment(**data)
@wled_exception_handler @wled_exception_handler
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
@@ -248,7 +327,23 @@ class WLEDLight(LightEntity, WLEDDeviceEntity):
else: else:
data[ATTR_COLOR_PRIMARY] += (self.white_value,) data[ATTR_COLOR_PRIMARY] += (self.white_value,)
await self.coordinator.wled.light(**data) # When only 1 segment is present, switch along the master, and use
# the master for power/brightness control.
if len(self.coordinator.data.state.segments) == 1:
master_data = {ATTR_ON: True}
if ATTR_BRIGHTNESS in data:
master_data[ATTR_BRIGHTNESS] = data[ATTR_BRIGHTNESS]
data[ATTR_BRIGHTNESS] = 255
if ATTR_TRANSITION in data:
master_data[ATTR_TRANSITION] = data[ATTR_TRANSITION]
del data[ATTR_TRANSITION]
await self.coordinator.wled.segment(**data)
await self.coordinator.wled.master(**master_data)
return
await self.coordinator.wled.segment(**data)
@wled_exception_handler @wled_exception_handler
async def async_effect( async def async_effect(
@@ -273,45 +368,59 @@ class WLEDLight(LightEntity, WLEDDeviceEntity):
if speed is not None: if speed is not None:
data[ATTR_SPEED] = speed data[ATTR_SPEED] = speed
await self.coordinator.wled.light(**data) await self.coordinator.wled.segment(**data)
@callback @callback
def async_update_segments( def async_update_segments(
entry: ConfigEntry, entry: ConfigEntry,
coordinator: WLEDDataUpdateCoordinator, coordinator: WLEDDataUpdateCoordinator,
current: Dict[int, WLEDLight], current: Dict[int, WLEDSegmentLight],
async_add_entities, async_add_entities,
) -> None: ) -> None:
"""Update segments.""" """Update segments."""
segment_ids = {light.segment_id for light in coordinator.data.state.segments} segment_ids = {light.segment_id for light in coordinator.data.state.segments}
current_ids = set(current) current_ids = set(current)
# Process new segments, add them to Home Assistant # Discard master (if present)
new_segments = [] current_ids.discard(-1)
for segment_id in segment_ids - current_ids:
current[segment_id] = WLEDLight(entry.entry_id, coordinator, segment_id)
new_segments.append(current[segment_id])
if new_segments: # Process new segments, add them to Home Assistant
async_add_entities(new_segments) new_entities = []
for segment_id in segment_ids - current_ids:
current[segment_id] = WLEDSegmentLight(entry.entry_id, coordinator, segment_id)
new_entities.append(current[segment_id])
# More than 1 segment now? Add master controls
if len(current_ids) < 2 and len(segment_ids) > 1:
current[-1] = WLEDMasterLight(entry.entry_id, coordinator)
new_entities.append(current[-1])
if new_entities:
async_add_entities(new_entities)
# Process deleted segments, remove them from Home Assistant # Process deleted segments, remove them from Home Assistant
for segment_id in current_ids - segment_ids: for segment_id in current_ids - segment_ids:
coordinator.hass.async_create_task( coordinator.hass.async_create_task(
async_remove_segment(segment_id, coordinator, current) async_remove_entity(segment_id, coordinator, current)
)
# Remove master if there is only 1 segment left
if len(current_ids) > 1 and len(segment_ids) < 2:
coordinator.hass.async_create_task(
async_remove_entity(-1, coordinator, current)
) )
async def async_remove_segment( async def async_remove_entity(
segment_id: int, index: int,
coordinator: WLEDDataUpdateCoordinator, coordinator: WLEDDataUpdateCoordinator,
current: Dict[int, WLEDLight], current: Dict[int, WLEDSegmentLight],
) -> None: ) -> None:
"""Remove WLED segment light from Home Assistant.""" """Remove WLED segment light from Home Assistant."""
entity = current[segment_id] entity = current[index]
await entity.async_remove() await entity.async_remove()
registry = await async_get_entity_registry(coordinator.hass) registry = await async_get_entity_registry(coordinator.hass)
if entity.entity_id in registry.entities: if entity.entity_id in registry.entities:
registry.async_remove(entity.entity_id) registry.async_remove(entity.entity_id)
del current[segment_id] del current[index]

View File

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

View File

@@ -7,7 +7,7 @@
"bellows==0.16.2", "bellows==0.16.2",
"pyserial==3.4", "pyserial==3.4",
"zha-quirks==0.0.39", "zha-quirks==0.0.39",
"zigpy-cc==0.4.2", "zigpy-cc==0.4.4",
"zigpy-deconz==0.9.2", "zigpy-deconz==0.9.2",
"zigpy==0.20.4", "zigpy==0.20.4",
"zigpy-xbee==0.12.1", "zigpy-xbee==0.12.1",

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 = 111 MINOR_VERSION = 111
PATCH_VERSION = "0b2" PATCH_VERSION = "0b4"
__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,10 +12,10 @@ cryptography==2.9.2
defusedxml==0.6.0 defusedxml==0.6.0
distro==1.5.0 distro==1.5.0
hass-nabucasa==0.34.5 hass-nabucasa==0.34.5
home-assistant-frontend==20200603.1 home-assistant-frontend==20200603.2
importlib-metadata==1.6.0 importlib-metadata==1.6.0
jinja2>=2.11.1 jinja2>=2.11.1
netdisco==2.6.0 netdisco==2.7.0
pip>=8.0.3 pip>=8.0.3
python-slugify==4.0.0 python-slugify==4.0.0
pytz>=2020.1 pytz>=2020.1

View File

@@ -734,7 +734,7 @@ hole==0.5.1
holidays==0.10.2 holidays==0.10.2
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20200603.1 home-assistant-frontend==20200603.2
# homeassistant.components.zwave # homeassistant.components.zwave
homeassistant-pyozw==0.1.10 homeassistant-pyozw==0.1.10
@@ -946,7 +946,7 @@ netdata==0.1.2
# homeassistant.components.discovery # homeassistant.components.discovery
# homeassistant.components.ssdp # homeassistant.components.ssdp
netdisco==2.6.0 netdisco==2.7.0
# homeassistant.components.neurio_energy # homeassistant.components.neurio_energy
neurio==0.3.1 neurio==0.3.1
@@ -1245,7 +1245,7 @@ pycfdns==0.0.1
pychannels==1.0.0 pychannels==1.0.0
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==5.3.0 pychromecast==6.0.0
# homeassistant.components.cmus # homeassistant.components.cmus
pycmus==0.1.1 pycmus==0.1.1
@@ -2195,7 +2195,7 @@ wirelesstagpy==0.4.0
withings-api==2.1.3 withings-api==2.1.3
# homeassistant.components.wled # homeassistant.components.wled
wled==0.4.1 wled==0.4.2
# homeassistant.components.xbee # homeassistant.components.xbee
xbee-helper==0.0.7 xbee-helper==0.0.7
@@ -2251,7 +2251,7 @@ zhong_hong_hvac==1.0.9
ziggo-mediabox-xl==1.1.0 ziggo-mediabox-xl==1.1.0
# homeassistant.components.zha # homeassistant.components.zha
zigpy-cc==0.4.2 zigpy-cc==0.4.4
# homeassistant.components.zha # homeassistant.components.zha
zigpy-deconz==0.9.2 zigpy-deconz==0.9.2

View File

@@ -321,7 +321,7 @@ hole==0.5.1
holidays==0.10.2 holidays==0.10.2
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20200603.1 home-assistant-frontend==20200603.2
# homeassistant.components.zwave # homeassistant.components.zwave
homeassistant-pyozw==0.1.10 homeassistant-pyozw==0.1.10
@@ -399,7 +399,7 @@ nessclient==0.9.15
# homeassistant.components.discovery # homeassistant.components.discovery
# homeassistant.components.ssdp # homeassistant.components.ssdp
netdisco==2.6.0 netdisco==2.7.0
# homeassistant.components.nexia # homeassistant.components.nexia
nexia==0.9.3 nexia==0.9.3
@@ -539,7 +539,7 @@ pyblackbird==0.5
pybotvac==0.0.17 pybotvac==0.0.17
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==5.3.0 pychromecast==6.0.0
# homeassistant.components.coolmaster # homeassistant.components.coolmaster
pycoolmasternet==0.0.4 pycoolmasternet==0.0.4
@@ -901,7 +901,7 @@ wiffi==1.0.0
withings-api==2.1.3 withings-api==2.1.3
# homeassistant.components.wled # homeassistant.components.wled
wled==0.4.1 wled==0.4.2
# homeassistant.components.bluesound # homeassistant.components.bluesound
# homeassistant.components.rest # homeassistant.components.rest
@@ -921,7 +921,7 @@ zeroconf==0.27.1
zha-quirks==0.0.39 zha-quirks==0.0.39
# homeassistant.components.zha # homeassistant.components.zha
zigpy-cc==0.4.2 zigpy-cc==0.4.4
# homeassistant.components.zha # homeassistant.components.zha
zigpy-deconz==0.9.2 zigpy-deconz==0.9.2

View File

@@ -80,17 +80,17 @@ async def async_setup_cast_internal_discovery(hass, config=None, discovery_info=
browser = MagicMock(zc={}) browser = MagicMock(zc={})
with patch( with patch(
"homeassistant.components.cast.discovery.pychromecast.discovery.CastListener", "homeassistant.components.cast.discovery.pychromecast.CastListener",
return_value=listener, return_value=listener,
) as cast_listener, patch( ) as cast_listener, patch(
"homeassistant.components.cast.discovery.zeroconf.ServiceBrowser", "homeassistant.components.cast.discovery.pychromecast.start_discovery",
return_value=browser, return_value=browser,
): ) as start_discovery:
add_entities = await async_setup_cast(hass, config, discovery_info) add_entities = await async_setup_cast(hass, config, discovery_info)
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.async_block_till_done() await hass.async_block_till_done()
assert cast_listener.call_count == 1 assert start_discovery.call_count == 1
discovery_callback = cast_listener.call_args[0][0] discovery_callback = cast_listener.call_args[0][0]
@@ -120,10 +120,10 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas
"homeassistant.components.cast.discovery.pychromecast.get_chromecast_from_service", "homeassistant.components.cast.discovery.pychromecast.get_chromecast_from_service",
return_value=chromecast, return_value=chromecast,
) as get_chromecast, patch( ) as get_chromecast, patch(
"homeassistant.components.cast.discovery.pychromecast.discovery.CastListener", "homeassistant.components.cast.discovery.pychromecast.CastListener",
return_value=listener, return_value=listener,
) as cast_listener, patch( ) as cast_listener, patch(
"homeassistant.components.cast.discovery.zeroconf.ServiceBrowser", "homeassistant.components.cast.discovery.pychromecast.start_discovery",
return_value=browser, return_value=browser,
): ):
await async_setup_component( await async_setup_component(
@@ -159,17 +159,15 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas
async def test_start_discovery_called_once(hass): async def test_start_discovery_called_once(hass):
"""Test pychromecast.start_discovery called exactly once.""" """Test pychromecast.start_discovery called exactly once."""
with patch( with patch(
"homeassistant.components.cast.discovery.pychromecast.discovery.CastListener", "homeassistant.components.cast.discovery.pychromecast.start_discovery",
) as cast_listener, patch(
"homeassistant.components.cast.discovery.zeroconf.ServiceBrowser",
return_value=Mock(), return_value=Mock(),
): ) as start_discovery:
await async_setup_cast(hass) await async_setup_cast(hass)
assert cast_listener.call_count == 1 assert start_discovery.call_count == 1
await async_setup_cast(hass) await async_setup_cast(hass)
assert cast_listener.call_count == 1 assert start_discovery.call_count == 1
async def test_stop_discovery_called_on_stop(hass): async def test_stop_discovery_called_on_stop(hass):
@@ -177,15 +175,13 @@ async def test_stop_discovery_called_on_stop(hass):
browser = MagicMock(zc={}) browser = MagicMock(zc={})
with patch( with patch(
"homeassistant.components.cast.discovery.pychromecast.discovery.CastListener", "homeassistant.components.cast.discovery.pychromecast.start_discovery",
) as cast_listener, patch(
"homeassistant.components.cast.discovery.zeroconf.ServiceBrowser",
return_value=browser, return_value=browser,
): ) as start_discovery:
# start_discovery should be called with empty config # start_discovery should be called with empty config
await async_setup_cast(hass, {}) await async_setup_cast(hass, {})
assert cast_listener.call_count == 1 assert start_discovery.call_count == 1
with patch( with patch(
"homeassistant.components.cast.discovery.pychromecast.stop_discovery" "homeassistant.components.cast.discovery.pychromecast.stop_discovery"
@@ -197,15 +193,13 @@ async def test_stop_discovery_called_on_stop(hass):
stop_discovery.assert_called_once_with(browser) stop_discovery.assert_called_once_with(browser)
with patch( with patch(
"homeassistant.components.cast.discovery.pychromecast.discovery.CastListener", "homeassistant.components.cast.discovery.pychromecast.start_discovery",
) as cast_listener, patch(
"homeassistant.components.cast.discovery.zeroconf.ServiceBrowser",
return_value=browser, return_value=browser,
): ) as start_discovery:
# start_discovery should be called again on re-startup # start_discovery should be called again on re-startup
await async_setup_cast(hass) await async_setup_cast(hass)
assert cast_listener.call_count == 1 assert start_discovery.call_count == 1
async def test_create_cast_device_without_uuid(hass): async def test_create_cast_device_without_uuid(hass):

View File

@@ -1,12 +1,13 @@
"""Fixtures for tests.""" """Fixtures for tests."""
from mock import patch
import pytest import pytest
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .common import ComponentFactory from .common import ComponentFactory
from tests.async_mock import patch
@pytest.fixture() @pytest.fixture()
def component_factory(hass: HomeAssistant): def component_factory(hass: HomeAssistant):

View File

@@ -4,8 +4,6 @@ import os
import shutil import shutil
from urllib.parse import urlencode from urllib.parse import urlencode
from mock import Mock, patch
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_ID,
DOMAIN as DOMAIN_MP, DOMAIN as DOMAIN_MP,
@@ -16,6 +14,7 @@ from homeassistant.config import async_process_ha_core_config
from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR
from homeassistant.setup import setup_component from homeassistant.setup import setup_component
from tests.async_mock import Mock, patch
from tests.common import assert_setup_component, get_test_home_assistant, mock_service from tests.common import assert_setup_component, get_test_home_assistant, mock_service

View File

@@ -2,7 +2,7 @@
import logging import logging
from homeassistant.const import UNIT_PERCENTAGE from homeassistant.const import STATE_UNKNOWN, UNIT_PERCENTAGE
from homeassistant.helpers import device_registry from homeassistant.helpers import device_registry
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -95,8 +95,8 @@ async def test_sensor_must_register(hass, create_registrations, webhook_client):
assert json["battery_state"]["error"]["code"] == "not_registered" assert json["battery_state"]["error"]["code"] == "not_registered"
async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client): async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client, caplog):
"""Test that sensors must have a unique ID.""" """Test that a duplicate unique ID in registration updates the sensor."""
webhook_id = create_registrations[1]["webhook_id"] webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}" webhook_url = f"/api/webhook/{webhook_id}"
@@ -120,11 +120,115 @@ async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client):
reg_json = await reg_resp.json() reg_json = await reg_resp.json()
assert reg_json == {"success": True} assert reg_json == {"success": True}
await hass.async_block_till_done()
assert "Re-register existing sensor" not in caplog.text
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.attributes["device_class"] == "battery"
assert entity.attributes["icon"] == "mdi:battery"
assert entity.attributes["unit_of_measurement"] == UNIT_PERCENTAGE
assert entity.attributes["foo"] == "bar"
assert entity.domain == "sensor"
assert entity.name == "Test 1 Battery State"
assert entity.state == "100"
payload["data"]["state"] = 99
dupe_resp = await webhook_client.post(webhook_url, json=payload) dupe_resp = await webhook_client.post(webhook_url, json=payload)
assert dupe_resp.status == 409 assert dupe_resp.status == 201
dupe_reg_json = await dupe_resp.json()
assert dupe_reg_json == {"success": True}
await hass.async_block_till_done()
dupe_json = await dupe_resp.json() assert "Re-register existing sensor" in caplog.text
assert dupe_json["success"] is False
assert dupe_json["error"]["code"] == "duplicate_unique_id" entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.attributes["device_class"] == "battery"
assert entity.attributes["icon"] == "mdi:battery"
assert entity.attributes["unit_of_measurement"] == UNIT_PERCENTAGE
assert entity.attributes["foo"] == "bar"
assert entity.domain == "sensor"
assert entity.name == "Test 1 Battery State"
assert entity.state == "99"
async def test_register_sensor_no_state(hass, create_registrations, webhook_client):
"""Test that sensors can be registered, when there is no (unknown) state."""
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": None,
"type": "sensor",
"unique_id": "battery_state",
},
},
)
assert reg_resp.status == 201
json = await reg_resp.json()
assert json == {"success": True}
await hass.async_block_till_done()
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.domain == "sensor"
assert entity.name == "Test 1 Battery State"
assert entity.state == STATE_UNKNOWN
async def test_update_sensor_no_state(hass, create_registrations, webhook_client):
"""Test that sensors can be updated, when there is no (unknown) state."""
webhook_id = create_registrations[1]["webhook_id"]
webhook_url = f"/api/webhook/{webhook_id}"
reg_resp = await webhook_client.post(
webhook_url,
json={
"type": "register_sensor",
"data": {
"name": "Battery State",
"state": 100,
"type": "sensor",
"unique_id": "battery_state",
},
},
)
assert reg_resp.status == 201
json = await reg_resp.json()
assert json == {"success": True}
await hass.async_block_till_done()
entity = hass.states.get("sensor.test_1_battery_state")
assert entity is not None
assert entity.state == "100"
update_resp = await webhook_client.post(
webhook_url,
json={
"type": "update_sensor_states",
"data": [{"state": None, "type": "sensor", "unique_id": "battery_state"}],
},
)
assert update_resp.status == 200
json = await update_resp.json()
assert json == {"battery_state": {"success": True}}
updated_entity = hass.states.get("sensor.test_1_battery_state")
assert updated_entity.state == STATE_UNKNOWN

View File

@@ -2,7 +2,6 @@
import datetime import datetime
from typing import Union from typing import Union
import mock
from py17track.package import Package from py17track.package import Package
import pytest import pytest
@@ -14,6 +13,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import utcnow from homeassistant.util import utcnow
from tests.async_mock import MagicMock, patch
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
VALID_CONFIG_MINIMAL = { VALID_CONFIG_MINIMAL = {
@@ -113,7 +113,7 @@ class ProfileMock:
@pytest.fixture(autouse=True, name="mock_client") @pytest.fixture(autouse=True, name="mock_client")
def fixture_mock_client(): def fixture_mock_client():
"""Mock py17track client.""" """Mock py17track client."""
with mock.patch( with patch(
"homeassistant.components.seventeentrack.sensor.SeventeenTrackClient", "homeassistant.components.seventeentrack.sensor.SeventeenTrackClient",
new=ClientMock, new=ClientMock,
): ):
@@ -137,7 +137,7 @@ async def _goto_future(hass, future=None):
"""Move to future.""" """Move to future."""
if not future: if not future:
future = utcnow() + datetime.timedelta(minutes=10) future = utcnow() + datetime.timedelta(minutes=10)
with mock.patch("homeassistant.util.utcnow", return_value=future): with patch("homeassistant.util.utcnow", return_value=future):
async_fire_time_changed(hass, future) async_fire_time_changed(hass, future)
await hass.async_block_till_done() await hass.async_block_till_done()
@@ -245,7 +245,7 @@ async def test_delivered_not_shown(hass):
) )
ProfileMock.package_list = [package] ProfileMock.package_list = [package]
hass.components.persistent_notification = mock.MagicMock() hass.components.persistent_notification = MagicMock()
await _setup_seventeentrack(hass, VALID_CONFIG_FULL_NO_DELIVERED) await _setup_seventeentrack(hass, VALID_CONFIG_FULL_NO_DELIVERED)
assert not hass.states.async_entity_ids() assert not hass.states.async_entity_ids()
hass.components.persistent_notification.create.assert_called() hass.components.persistent_notification.create.assert_called()
@@ -258,7 +258,7 @@ async def test_delivered_shown(hass):
) )
ProfileMock.package_list = [package] ProfileMock.package_list = [package]
hass.components.persistent_notification = mock.MagicMock() hass.components.persistent_notification = MagicMock()
await _setup_seventeentrack(hass, VALID_CONFIG_FULL) await _setup_seventeentrack(hass, VALID_CONFIG_FULL)
assert hass.states.get("sensor.seventeentrack_package_456") is not None assert hass.states.get("sensor.seventeentrack_package_456") is not None
@@ -283,7 +283,7 @@ async def test_becomes_delivered_not_shown_notification(hass):
) )
ProfileMock.package_list = [package_delivered] ProfileMock.package_list = [package_delivered]
hass.components.persistent_notification = mock.MagicMock() hass.components.persistent_notification = MagicMock()
await _goto_future(hass) await _goto_future(hass)
hass.components.persistent_notification.create.assert_called() hass.components.persistent_notification.create.assert_called()

View File

@@ -2,13 +2,13 @@
from typing import Callable, Dict, NamedTuple, Tuple from typing import Callable, Dict, NamedTuple, Tuple
from mock import MagicMock
import pyvera as pv import pyvera as pv
from homeassistant.components.vera.const import CONF_CONTROLLER, DOMAIN from homeassistant.components.vera.const import CONF_CONTROLLER, DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.async_mock import MagicMock
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
SetupCallback = Callable[[pv.VeraController, dict], None] SetupCallback = Callable[[pv.VeraController, dict], None]

View File

@@ -1,10 +1,11 @@
"""Fixtures for tests.""" """Fixtures for tests."""
from mock import patch
import pytest import pytest
from .common import ComponentFactory from .common import ComponentFactory
from tests.async_mock import patch
@pytest.fixture() @pytest.fixture()
def vera_component_factory(): def vera_component_factory():

View File

@@ -1,5 +1,4 @@
"""Vera tests.""" """Vera tests."""
from mock import patch
from requests.exceptions import RequestException from requests.exceptions import RequestException
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
@@ -12,7 +11,7 @@ from homeassistant.data_entry_flow import (
RESULT_TYPE_FORM, RESULT_TYPE_FORM,
) )
from tests.async_mock import MagicMock from tests.async_mock import MagicMock, patch
from tests.common import MockConfigEntry from tests.common import MockConfigEntry

View File

@@ -29,6 +29,7 @@ from homeassistant.const import (
ATTR_ICON, ATTR_ICON,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_OFF,
STATE_ON, STATE_ON,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
) )
@@ -50,7 +51,7 @@ async def test_rgb_light_state(
entity_registry = await hass.helpers.entity_registry.async_get_registry() entity_registry = await hass.helpers.entity_registry.async_get_registry()
# First segment of the strip # First segment of the strip
state = hass.states.get("light.wled_rgb_light") state = hass.states.get("light.wled_rgb_light_segment_0")
assert state assert state
assert state.attributes.get(ATTR_BRIGHTNESS) == 127 assert state.attributes.get(ATTR_BRIGHTNESS) == 127
assert state.attributes.get(ATTR_EFFECT) == "Solid" assert state.attributes.get(ATTR_EFFECT) == "Solid"
@@ -64,12 +65,12 @@ async def test_rgb_light_state(
assert state.attributes.get(ATTR_SPEED) == 32 assert state.attributes.get(ATTR_SPEED) == 32
assert state.state == STATE_ON assert state.state == STATE_ON
entry = entity_registry.async_get("light.wled_rgb_light") entry = entity_registry.async_get("light.wled_rgb_light_segment_0")
assert entry assert entry
assert entry.unique_id == "aabbccddeeff_0" assert entry.unique_id == "aabbccddeeff_0"
# Second segment of the strip # Second segment of the strip
state = hass.states.get("light.wled_rgb_light_1") state = hass.states.get("light.wled_rgb_light_segment_1")
assert state assert state
assert state.attributes.get(ATTR_BRIGHTNESS) == 127 assert state.attributes.get(ATTR_BRIGHTNESS) == 127
assert state.attributes.get(ATTR_EFFECT) == "Blink" assert state.attributes.get(ATTR_EFFECT) == "Blink"
@@ -83,22 +84,32 @@ async def test_rgb_light_state(
assert state.attributes.get(ATTR_SPEED) == 16 assert state.attributes.get(ATTR_SPEED) == 16
assert state.state == STATE_ON assert state.state == STATE_ON
entry = entity_registry.async_get("light.wled_rgb_light_1") entry = entity_registry.async_get("light.wled_rgb_light_segment_1")
assert entry assert entry
assert entry.unique_id == "aabbccddeeff_1" assert entry.unique_id == "aabbccddeeff_1"
# Test master control of the lightstrip
state = hass.states.get("light.wled_rgb_light_master")
assert state
assert state.attributes.get(ATTR_BRIGHTNESS) == 127
assert state.state == STATE_ON
async def test_switch_change_state( entry = entity_registry.async_get("light.wled_rgb_light_master")
assert entry
assert entry.unique_id == "aabbccddeeff"
async def test_segment_change_state(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
) -> None: ) -> None:
"""Test the change of state of the WLED switches.""" """Test the change of state of the WLED segments."""
await init_integration(hass, aioclient_mock) await init_integration(hass, aioclient_mock)
with patch("wled.WLED.light") as light_mock: with patch("wled.WLED.segment") as light_mock:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_TRANSITION: 5}, {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_TRANSITION: 5},
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@@ -106,14 +117,14 @@ async def test_switch_change_state(
on=False, segment_id=0, transition=50, on=False, segment_id=0, transition=50,
) )
with patch("wled.WLED.light") as light_mock: with patch("wled.WLED.segment") as light_mock:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
{ {
ATTR_BRIGHTNESS: 42, ATTR_BRIGHTNESS: 42,
ATTR_EFFECT: "Chase", ATTR_EFFECT: "Chase",
ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
ATTR_RGB_COLOR: [255, 0, 0], ATTR_RGB_COLOR: [255, 0, 0],
ATTR_TRANSITION: 5, ATTR_TRANSITION: 5,
}, },
@@ -129,11 +140,11 @@ async def test_switch_change_state(
transition=50, transition=50,
) )
with patch("wled.WLED.light") as light_mock: with patch("wled.WLED.segment") as light_mock:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_COLOR_TEMP: 400}, {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_COLOR_TEMP: 400},
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@@ -142,33 +153,178 @@ async def test_switch_change_state(
) )
async def test_master_change_state(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
) -> None:
"""Test the change of state of the WLED master light control."""
await init_integration(hass, aioclient_mock)
with patch("wled.WLED.master") as light_mock:
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.wled_rgb_light_master", ATTR_TRANSITION: 5},
blocking=True,
)
await hass.async_block_till_done()
light_mock.assert_called_once_with(
on=False, transition=50,
)
with patch("wled.WLED.master") as light_mock:
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_BRIGHTNESS: 42,
ATTR_ENTITY_ID: "light.wled_rgb_light_master",
ATTR_TRANSITION: 5,
},
blocking=True,
)
await hass.async_block_till_done()
light_mock.assert_called_once_with(
brightness=42, on=True, transition=50,
)
with patch("wled.WLED.master") as light_mock:
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.wled_rgb_light_master", ATTR_TRANSITION: 5},
blocking=True,
)
await hass.async_block_till_done()
light_mock.assert_called_once_with(
on=False, transition=50,
)
with patch("wled.WLED.master") as light_mock:
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_BRIGHTNESS: 42,
ATTR_ENTITY_ID: "light.wled_rgb_light_master",
ATTR_TRANSITION: 5,
},
blocking=True,
)
await hass.async_block_till_done()
light_mock.assert_called_once_with(
brightness=42, on=True, transition=50,
)
async def test_dynamically_handle_segments( async def test_dynamically_handle_segments(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None: ) -> None:
"""Test if a new/deleted segment is dynamically added/removed.""" """Test if a new/deleted segment is dynamically added/removed."""
await init_integration(hass, aioclient_mock) await init_integration(hass, aioclient_mock)
assert hass.states.get("light.wled_rgb_light") assert hass.states.get("light.wled_rgb_light_master")
assert hass.states.get("light.wled_rgb_light_1") assert hass.states.get("light.wled_rgb_light_segment_0")
assert hass.states.get("light.wled_rgb_light_segment_1")
data = json.loads(load_fixture("wled/rgb_single_segment.json")) data = json.loads(load_fixture("wled/rgb_single_segment.json"))
device = WLEDDevice(data) device = WLEDDevice(data)
# Test removal if segment went missing # Test removal if segment went missing, including the master entity
with patch( with patch(
"homeassistant.components.wled.WLED.update", return_value=device, "homeassistant.components.wled.WLED.update", return_value=device,
): ):
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("light.wled_rgb_light") assert hass.states.get("light.wled_rgb_light_segment_0")
assert not hass.states.get("light.wled_rgb_light_1") assert not hass.states.get("light.wled_rgb_light_segment_1")
assert not hass.states.get("light.wled_rgb_light_master")
# Test adding if segment shows up again # Test adding if segment shows up again, including the master entity
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get("light.wled_rgb_light") assert hass.states.get("light.wled_rgb_light_master")
assert hass.states.get("light.wled_rgb_light_1") assert hass.states.get("light.wled_rgb_light_segment_0")
assert hass.states.get("light.wled_rgb_light_segment_1")
async def test_single_segment_behavior(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
) -> None:
"""Test the behavior of the integration with a single segment."""
await init_integration(hass, aioclient_mock)
data = json.loads(load_fixture("wled/rgb_single_segment.json"))
device = WLEDDevice(data)
# Test absent master
with patch(
"homeassistant.components.wled.WLED.update", return_value=device,
):
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
assert not hass.states.get("light.wled_rgb_light_master")
state = hass.states.get("light.wled_rgb_light_segment_0")
assert state
assert state.state == STATE_ON
# Test segment brightness takes master into account
device.state.brightness = 100
device.state.segments[0].brightness = 255
with patch(
"homeassistant.components.wled.WLED.update", return_value=device,
):
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
state = hass.states.get("light.wled_rgb_light_segment_0")
assert state
assert state.attributes.get(ATTR_BRIGHTNESS) == 100
# Test segment is off when master is off
device.state.on = False
with patch(
"homeassistant.components.wled.WLED.update", return_value=device,
):
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
state = hass.states.get("light.wled_rgb_light_segment_0")
assert state
assert state.state == STATE_OFF
# Test master is turned off when turning off a single segment
with patch("wled.WLED.master") as master_mock:
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_TRANSITION: 5},
blocking=True,
)
await hass.async_block_till_done()
master_mock.assert_called_once_with(
on=False, transition=50,
)
# Test master is turned on when turning on a single segment, and segment
# brightness is set to 255.
with patch("wled.WLED.master") as master_mock, patch(
"wled.WLED.segment"
) as segment_mock:
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
ATTR_TRANSITION: 5,
ATTR_BRIGHTNESS: 42,
},
blocking=True,
)
await hass.async_block_till_done()
master_mock.assert_called_once_with(on=True, transition=50, brightness=42)
segment_mock.assert_called_once_with(on=True, segment_id=0, brightness=255)
async def test_light_error( async def test_light_error(
@@ -182,12 +338,12 @@ async def test_light_error(
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.wled_rgb_light"}, {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0"},
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("light.wled_rgb_light") state = hass.states.get("light.wled_rgb_light_segment_0")
assert state.state == STATE_ON assert state.state == STATE_ON
assert "Invalid response from API" in caplog.text assert "Invalid response from API" in caplog.text
@@ -199,17 +355,17 @@ async def test_light_connection_error(
await init_integration(hass, aioclient_mock) await init_integration(hass, aioclient_mock)
with patch("homeassistant.components.wled.WLED.update"), patch( with patch("homeassistant.components.wled.WLED.update"), patch(
"homeassistant.components.wled.WLED.light", side_effect=WLEDConnectionError "homeassistant.components.wled.WLED.segment", side_effect=WLEDConnectionError
): ):
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.wled_rgb_light"}, {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0"},
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("light.wled_rgb_light") state = hass.states.get("light.wled_rgb_light_segment_0")
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
@@ -224,7 +380,7 @@ async def test_rgbw_light(
assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0) assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0)
assert state.attributes.get(ATTR_WHITE_VALUE) == 139 assert state.attributes.get(ATTR_WHITE_VALUE) == 139
with patch("wled.WLED.light") as light_mock: with patch("wled.WLED.segment") as light_mock:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
@@ -236,7 +392,7 @@ async def test_rgbw_light(
on=True, segment_id=0, color_primary=(255, 159, 70, 139), on=True, segment_id=0, color_primary=(255, 159, 70, 139),
) )
with patch("wled.WLED.light") as light_mock: with patch("wled.WLED.segment") as light_mock:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
@@ -248,7 +404,7 @@ async def test_rgbw_light(
color_primary=(255, 0, 0, 100), on=True, segment_id=0, color_primary=(255, 0, 0, 100), on=True, segment_id=0,
) )
with patch("wled.WLED.light") as light_mock: with patch("wled.WLED.segment") as light_mock:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
SERVICE_TURN_ON, SERVICE_TURN_ON,
@@ -271,13 +427,13 @@ async def test_effect_service(
"""Test the effect service of a WLED light.""" """Test the effect service of a WLED light."""
await init_integration(hass, aioclient_mock) await init_integration(hass, aioclient_mock)
with patch("wled.WLED.light") as light_mock: with patch("wled.WLED.segment") as light_mock:
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_EFFECT, SERVICE_EFFECT,
{ {
ATTR_EFFECT: "Rainbow", ATTR_EFFECT: "Rainbow",
ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
ATTR_INTENSITY: 200, ATTR_INTENSITY: 200,
ATTR_REVERSE: True, ATTR_REVERSE: True,
ATTR_SPEED: 100, ATTR_SPEED: 100,
@@ -289,11 +445,11 @@ async def test_effect_service(
effect="Rainbow", intensity=200, reverse=True, segment_id=0, speed=100, effect="Rainbow", intensity=200, reverse=True, segment_id=0, speed=100,
) )
with patch("wled.WLED.light") as light_mock: with patch("wled.WLED.segment") as light_mock:
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_EFFECT, SERVICE_EFFECT,
{ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_EFFECT: 9}, {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_EFFECT: 9},
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@@ -301,12 +457,12 @@ async def test_effect_service(
segment_id=0, effect=9, segment_id=0, effect=9,
) )
with patch("wled.WLED.light") as light_mock: with patch("wled.WLED.segment") as light_mock:
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_EFFECT, SERVICE_EFFECT,
{ {
ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
ATTR_INTENSITY: 200, ATTR_INTENSITY: 200,
ATTR_REVERSE: True, ATTR_REVERSE: True,
ATTR_SPEED: 100, ATTR_SPEED: 100,
@@ -318,13 +474,13 @@ async def test_effect_service(
intensity=200, reverse=True, segment_id=0, speed=100, intensity=200, reverse=True, segment_id=0, speed=100,
) )
with patch("wled.WLED.light") as light_mock: with patch("wled.WLED.segment") as light_mock:
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_EFFECT, SERVICE_EFFECT,
{ {
ATTR_EFFECT: "Rainbow", ATTR_EFFECT: "Rainbow",
ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
ATTR_REVERSE: True, ATTR_REVERSE: True,
ATTR_SPEED: 100, ATTR_SPEED: 100,
}, },
@@ -335,13 +491,13 @@ async def test_effect_service(
effect="Rainbow", reverse=True, segment_id=0, speed=100, effect="Rainbow", reverse=True, segment_id=0, speed=100,
) )
with patch("wled.WLED.light") as light_mock: with patch("wled.WLED.segment") as light_mock:
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_EFFECT, SERVICE_EFFECT,
{ {
ATTR_EFFECT: "Rainbow", ATTR_EFFECT: "Rainbow",
ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
ATTR_INTENSITY: 200, ATTR_INTENSITY: 200,
ATTR_SPEED: 100, ATTR_SPEED: 100,
}, },
@@ -352,13 +508,13 @@ async def test_effect_service(
effect="Rainbow", intensity=200, segment_id=0, speed=100, effect="Rainbow", intensity=200, segment_id=0, speed=100,
) )
with patch("wled.WLED.light") as light_mock: with patch("wled.WLED.segment") as light_mock:
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_EFFECT, SERVICE_EFFECT,
{ {
ATTR_EFFECT: "Rainbow", ATTR_EFFECT: "Rainbow",
ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0",
ATTR_INTENSITY: 200, ATTR_INTENSITY: 200,
ATTR_REVERSE: True, ATTR_REVERSE: True,
}, },
@@ -381,11 +537,11 @@ async def test_effect_service_error(
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_EFFECT, SERVICE_EFFECT,
{ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_EFFECT: 9}, {ATTR_ENTITY_ID: "light.wled_rgb_light_segment_0", ATTR_EFFECT: 9},
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get("light.wled_rgb_light") state = hass.states.get("light.wled_rgb_light_segment_0")
assert state.state == STATE_ON assert state.state == STATE_ON
assert "Invalid response from API" in caplog.text assert "Invalid response from API" in caplog.text