forked from home-assistant/core
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f39296251 | ||
|
|
dd0fd36049 | ||
|
|
30a391b88b | ||
|
|
71803cbdef | ||
|
|
f5eafbe760 | ||
|
|
52f710528f | ||
|
|
38b729b00a | ||
|
|
5dae7f8451 | ||
|
|
6a297b3758 | ||
|
|
ab7afbdaf7 | ||
|
|
0763503151 | ||
|
|
4ead87270e | ||
|
|
1634592d90 | ||
|
|
ddddd8566d | ||
|
|
5cf2043c04 | ||
|
|
43777ace20 | ||
|
|
a7e5cc31c3 | ||
|
|
aa6520cac1 | ||
|
|
af10cd315e | ||
|
|
ef28bcaa9c | ||
|
|
ff3bfade31 | ||
|
|
4eafd8adf7 | ||
|
|
cb5de0e090 | ||
|
|
254394ecab | ||
|
|
e4e0c37a8c |
@@ -3,7 +3,7 @@
|
||||
"name": "AdGuard Home",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/adguard",
|
||||
"requirements": ["adguardhome==0.4.1"],
|
||||
"requirements": ["adguardhome==0.4.2"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@frenck"]
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,6 +32,9 @@ DEFAULT_SSH_PORT = 22
|
||||
DEFAULT_INTERFACE = "eth0"
|
||||
DEFAULT_DNSMASQ = "/var/lib/misc"
|
||||
|
||||
FIRST_RETRY_TIME = 60
|
||||
MAX_RETRY_TIME = 900
|
||||
|
||||
SECRET_GROUP = "Password or SSH Key"
|
||||
SENSOR_TYPES = ["upload_speed", "download_speed", "download", "upload"]
|
||||
|
||||
@@ -59,7 +63,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME):
|
||||
"""Set up the asuswrt component."""
|
||||
|
||||
conf = config[DOMAIN]
|
||||
@@ -77,9 +81,29 @@ async def async_setup(hass, config):
|
||||
dnsmasq=conf[CONF_DNSMASQ],
|
||||
)
|
||||
|
||||
await api.connection.async_connect()
|
||||
try:
|
||||
await api.connection.async_connect()
|
||||
except OSError as ex:
|
||||
_LOGGER.warning(
|
||||
"Error [%s] connecting %s to %s. Will retry in %s seconds...",
|
||||
str(ex),
|
||||
DOMAIN,
|
||||
conf[CONF_HOST],
|
||||
retry_delay,
|
||||
)
|
||||
|
||||
async def retry_setup(now):
|
||||
"""Retry setup if a error happens on asuswrt API."""
|
||||
await async_setup(
|
||||
hass, config, retry_delay=min(2 * retry_delay, MAX_RETRY_TIME)
|
||||
)
|
||||
|
||||
async_call_later(hass, retry_delay, retry_setup)
|
||||
|
||||
return True
|
||||
|
||||
if not api.is_connected:
|
||||
_LOGGER.error("Unable to setup component")
|
||||
_LOGGER.error("Error connecting %s to %s.", DOMAIN, conf[CONF_HOST])
|
||||
return False
|
||||
|
||||
hass.data[DATA_ASUSWRT] = api
|
||||
|
||||
@@ -218,8 +218,8 @@ class BraviaTVDevice(MediaPlayerDevice):
|
||||
self._channel_name = playing_info.get("title")
|
||||
self._program_media_type = playing_info.get("programMediaType")
|
||||
self._channel_number = playing_info.get("dispNum")
|
||||
self._source = playing_info.get("source")
|
||||
self._content_uri = playing_info.get("uri")
|
||||
self._source = self._get_source()
|
||||
self._duration = playing_info.get("durationSec")
|
||||
self._start_date_time = playing_info.get("startDateTime")
|
||||
else:
|
||||
@@ -229,6 +229,12 @@ class BraviaTVDevice(MediaPlayerDevice):
|
||||
_LOGGER.error(exception_instance)
|
||||
self._state = STATE_OFF
|
||||
|
||||
def _get_source(self):
|
||||
"""Return the name of the source."""
|
||||
for key, value in self._content_mapping.items():
|
||||
if value == self._content_uri:
|
||||
return key
|
||||
|
||||
def _reset_playing_info(self):
|
||||
self._program_name = None
|
||||
self._channel_name = None
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"title": "Connect to the DoorBird"
|
||||
}
|
||||
},
|
||||
"title": "DoorBird"
|
||||
"title": "DoorBird",
|
||||
"flow_title" : "DoorBird {name} ({host})"
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
@@ -33,4 +34,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"not_doorbird_device": "This device is not a DoorBird"
|
||||
},
|
||||
"title" : "DoorBird",
|
||||
"flow_title" : "DoorBird {name} ({host})",
|
||||
"error" : {
|
||||
"invalid_auth" : "Invalid authentication",
|
||||
"unknown" : "Unexpected error",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20200401.0"
|
||||
"home-assistant-frontend==20200403.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
||||
@@ -132,7 +132,9 @@ class GenericCamera(Camera):
|
||||
)
|
||||
return response.content
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error("Error getting camera image: %s", error)
|
||||
_LOGGER.error(
|
||||
"Error getting new camera image from %s: %s", self._name, error
|
||||
)
|
||||
return self._last_image
|
||||
|
||||
self._last_image = await self.hass.async_add_job(fetch)
|
||||
@@ -146,10 +148,12 @@ class GenericCamera(Camera):
|
||||
response = await websession.get(url, auth=self._auth)
|
||||
self._last_image = await response.read()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout getting image from: %s", self._name)
|
||||
_LOGGER.error("Timeout getting camera image from %s", self._name)
|
||||
return self._last_image
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Error getting new camera image: %s", err)
|
||||
_LOGGER.error(
|
||||
"Error getting new camera image from %s: %s", self._name, err
|
||||
)
|
||||
return self._last_image
|
||||
|
||||
self._last_url = url
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from async_timeout import timeout
|
||||
from gios import ApiError, Gios, NoStationError
|
||||
from gios import ApiError, Gios, InvalidSensorsData, NoStationError
|
||||
|
||||
from homeassistant.core import Config, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
@@ -63,7 +63,12 @@ class GiosDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
try:
|
||||
with timeout(30):
|
||||
await self.gios.update()
|
||||
except (ApiError, NoStationError, ClientConnectorError) as error:
|
||||
except (
|
||||
ApiError,
|
||||
NoStationError,
|
||||
ClientConnectorError,
|
||||
InvalidSensorsData,
|
||||
) as error:
|
||||
raise UpdateFailed(error)
|
||||
if not self.gios.data:
|
||||
raise UpdateFailed("Invalid sensors data")
|
||||
|
||||
@@ -3,10 +3,10 @@ import asyncio
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from async_timeout import timeout
|
||||
from gios import ApiError, Gios, NoStationError
|
||||
from gios import ApiError, Gios, InvalidSensorsData, NoStationError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, exceptions
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
@@ -43,9 +43,6 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
gios = Gios(user_input[CONF_STATION_ID], websession)
|
||||
await gios.update()
|
||||
|
||||
if not gios.available:
|
||||
raise InvalidSensorsData()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_STATION_ID], data=user_input,
|
||||
)
|
||||
@@ -59,7 +56,3 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
|
||||
class InvalidSensorsData(exceptions.HomeAssistantError):
|
||||
"""Error to indicate invalid sensors data."""
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/gios",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@bieniu"],
|
||||
"requirements": ["gios==0.0.5"],
|
||||
"requirements": ["gios==0.1.1"],
|
||||
"config_flow": true
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS
|
||||
from homeassistant.components.remote import (
|
||||
ATTR_ACTIVITY,
|
||||
ATTR_DELAY_SECS,
|
||||
DEFAULT_DELAY_SECS,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -33,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
address = entry.data[CONF_HOST]
|
||||
name = entry.data[CONF_NAME]
|
||||
activity = entry.options.get(ATTR_ACTIVITY)
|
||||
delay_secs = entry.options.get(ATTR_DELAY_SECS)
|
||||
delay_secs = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
|
||||
|
||||
harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf")
|
||||
try:
|
||||
|
||||
@@ -128,8 +128,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_import(self, validated_input):
|
||||
"""Handle import."""
|
||||
await self.async_set_unique_id(validated_input[UNIQUE_ID])
|
||||
await self.async_set_unique_id(
|
||||
validated_input[UNIQUE_ID], raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
# Everything was validated in remote async_setup_platform
|
||||
# all we do now is create.
|
||||
return await self._async_create_entry_from_valid_input(
|
||||
@@ -149,14 +152,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
# Options from yaml are preserved, we will pull them out when
|
||||
# we setup the config entry
|
||||
data.update(_options_from_user_input(user_input))
|
||||
return self.async_create_entry(title=validated[CONF_NAME], data=data)
|
||||
|
||||
def _host_already_configured(self, user_input):
|
||||
"""See if we already have a harmony matching user input configured."""
|
||||
existing_hosts = {
|
||||
entry.data[CONF_HOST] for entry in self._async_current_entries()
|
||||
}
|
||||
return user_input[CONF_HOST] in existing_hosts
|
||||
return self.async_create_entry(title=validated[CONF_NAME], data=data)
|
||||
|
||||
|
||||
def _options_from_user_input(user_input):
|
||||
|
||||
@@ -10,6 +10,7 @@ from homeassistant.components.http import (
|
||||
CONF_SERVER_HOST,
|
||||
CONF_SERVER_PORT,
|
||||
CONF_SSL_CERTIFICATE,
|
||||
DEFAULT_SERVER_HOST,
|
||||
)
|
||||
from homeassistant.const import SERVER_PORT
|
||||
|
||||
@@ -133,9 +134,14 @@ class HassIO:
|
||||
"refresh_token": refresh_token.token,
|
||||
}
|
||||
|
||||
if CONF_SERVER_HOST in http_config:
|
||||
if (
|
||||
http_config.get(CONF_SERVER_HOST, DEFAULT_SERVER_HOST)
|
||||
!= DEFAULT_SERVER_HOST
|
||||
):
|
||||
options["watchdog"] = False
|
||||
_LOGGER.warning("Don't use 'server_host' options with Hass.io")
|
||||
_LOGGER.warning(
|
||||
"Found incompatible HTTP option 'server_host'. Watchdog feature disabled"
|
||||
)
|
||||
|
||||
return await self.send_command("/homeassistant/options", payload=options)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
from . import TYPES
|
||||
from .accessories import HomeAccessory, debounce
|
||||
from .accessories import HomeAccessory
|
||||
from .const import (
|
||||
CHAR_BRIGHTNESS,
|
||||
CHAR_COLOR_TEMPERATURE,
|
||||
@@ -52,15 +52,6 @@ class Light(HomeAccessory):
|
||||
def __init__(self, *args):
|
||||
"""Initialize a new Light accessory object."""
|
||||
super().__init__(*args, category=CATEGORY_LIGHTBULB)
|
||||
self._flag = {
|
||||
CHAR_ON: False,
|
||||
CHAR_BRIGHTNESS: False,
|
||||
CHAR_HUE: False,
|
||||
CHAR_SATURATION: False,
|
||||
CHAR_COLOR_TEMPERATURE: False,
|
||||
RGB_COLOR: False,
|
||||
}
|
||||
self._state = 0
|
||||
|
||||
self.chars = []
|
||||
self._features = self.hass.states.get(self.entity_id).attributes.get(
|
||||
@@ -82,17 +73,14 @@ class Light(HomeAccessory):
|
||||
self.chars.append(CHAR_COLOR_TEMPERATURE)
|
||||
|
||||
serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars)
|
||||
self.char_on = serv_light.configure_char(
|
||||
CHAR_ON, value=self._state, setter_callback=self.set_state
|
||||
)
|
||||
|
||||
self.char_on = serv_light.configure_char(CHAR_ON, value=0)
|
||||
|
||||
if CHAR_BRIGHTNESS in self.chars:
|
||||
# Initial value is set to 100 because 0 is a special value (off). 100 is
|
||||
# an arbitrary non-zero value. It is updated immediately by update_state
|
||||
# to set to the correct initial value.
|
||||
self.char_brightness = serv_light.configure_char(
|
||||
CHAR_BRIGHTNESS, value=100, setter_callback=self.set_brightness
|
||||
)
|
||||
self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100)
|
||||
|
||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||
min_mireds = self.hass.states.get(self.entity_id).attributes.get(
|
||||
@@ -105,133 +93,94 @@ class Light(HomeAccessory):
|
||||
CHAR_COLOR_TEMPERATURE,
|
||||
value=min_mireds,
|
||||
properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds},
|
||||
setter_callback=self.set_color_temperature,
|
||||
)
|
||||
|
||||
if CHAR_HUE in self.chars:
|
||||
self.char_hue = serv_light.configure_char(
|
||||
CHAR_HUE, value=0, setter_callback=self.set_hue
|
||||
)
|
||||
self.char_hue = serv_light.configure_char(CHAR_HUE, value=0)
|
||||
|
||||
if CHAR_SATURATION in self.chars:
|
||||
self.char_saturation = serv_light.configure_char(
|
||||
CHAR_SATURATION, value=75, setter_callback=self.set_saturation
|
||||
)
|
||||
self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75)
|
||||
|
||||
def set_state(self, value):
|
||||
"""Set state if call came from HomeKit."""
|
||||
if self._state == value:
|
||||
return
|
||||
serv_light.setter_callback = self._set_chars
|
||||
|
||||
_LOGGER.debug("%s: Set state to %d", self.entity_id, value)
|
||||
self._flag[CHAR_ON] = True
|
||||
def _set_chars(self, char_values):
|
||||
_LOGGER.debug("_set_chars: %s", char_values)
|
||||
events = []
|
||||
service = SERVICE_TURN_ON
|
||||
params = {ATTR_ENTITY_ID: self.entity_id}
|
||||
service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF
|
||||
self.call_service(DOMAIN, service, params)
|
||||
if CHAR_ON in char_values:
|
||||
if not char_values[CHAR_ON]:
|
||||
service = SERVICE_TURN_OFF
|
||||
events.append(f"Set state to {char_values[CHAR_ON]}")
|
||||
|
||||
@debounce
|
||||
def set_brightness(self, value):
|
||||
"""Set brightness if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set brightness to %d", self.entity_id, value)
|
||||
self._flag[CHAR_BRIGHTNESS] = True
|
||||
if value == 0:
|
||||
self.set_state(0) # Turn off light
|
||||
return
|
||||
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value}
|
||||
self.call_service(DOMAIN, SERVICE_TURN_ON, params, f"brightness at {value}%")
|
||||
if CHAR_BRIGHTNESS in char_values:
|
||||
if char_values[CHAR_BRIGHTNESS] == 0:
|
||||
events[-1] = f"Set state to 0"
|
||||
service = SERVICE_TURN_OFF
|
||||
else:
|
||||
params[ATTR_BRIGHTNESS_PCT] = char_values[CHAR_BRIGHTNESS]
|
||||
events.append(f"brightness at {char_values[CHAR_BRIGHTNESS]}%")
|
||||
|
||||
def set_color_temperature(self, value):
|
||||
"""Set color temperature if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set color temp to %s", self.entity_id, value)
|
||||
self._flag[CHAR_COLOR_TEMPERATURE] = True
|
||||
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value}
|
||||
self.call_service(
|
||||
DOMAIN, SERVICE_TURN_ON, params, f"color temperature at {value}"
|
||||
)
|
||||
if CHAR_COLOR_TEMPERATURE in char_values:
|
||||
params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE]
|
||||
events.append(f"color temperature at {char_values[CHAR_COLOR_TEMPERATURE]}")
|
||||
|
||||
def set_saturation(self, value):
|
||||
"""Set saturation if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set saturation to %d", self.entity_id, value)
|
||||
self._flag[CHAR_SATURATION] = True
|
||||
self._saturation = value
|
||||
self.set_color()
|
||||
|
||||
def set_hue(self, value):
|
||||
"""Set hue if call came from HomeKit."""
|
||||
_LOGGER.debug("%s: Set hue to %d", self.entity_id, value)
|
||||
self._flag[CHAR_HUE] = True
|
||||
self._hue = value
|
||||
self.set_color()
|
||||
|
||||
def set_color(self):
|
||||
"""Set color if call came from HomeKit."""
|
||||
if (
|
||||
self._features & SUPPORT_COLOR
|
||||
and self._flag[CHAR_HUE]
|
||||
and self._flag[CHAR_SATURATION]
|
||||
and CHAR_HUE in char_values
|
||||
and CHAR_SATURATION in char_values
|
||||
):
|
||||
color = (self._hue, self._saturation)
|
||||
color = (char_values[CHAR_HUE], char_values[CHAR_SATURATION])
|
||||
_LOGGER.debug("%s: Set hs_color to %s", self.entity_id, color)
|
||||
self._flag.update(
|
||||
{CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}
|
||||
)
|
||||
params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color}
|
||||
self.call_service(DOMAIN, SERVICE_TURN_ON, params, f"set color at {color}")
|
||||
params[ATTR_HS_COLOR] = color
|
||||
events.append(f"set color at {color}")
|
||||
|
||||
self.call_service(DOMAIN, service, params, ", ".join(events))
|
||||
|
||||
def update_state(self, new_state):
|
||||
"""Update light after state change."""
|
||||
# Handle State
|
||||
state = new_state.state
|
||||
if state in (STATE_ON, STATE_OFF):
|
||||
self._state = 1 if state == STATE_ON else 0
|
||||
if not self._flag[CHAR_ON] and self.char_on.value != self._state:
|
||||
self.char_on.set_value(self._state)
|
||||
self._flag[CHAR_ON] = False
|
||||
if state == STATE_ON and self.char_on.value != 1:
|
||||
self.char_on.set_value(1)
|
||||
elif state == STATE_OFF and self.char_on.value != 0:
|
||||
self.char_on.set_value(0)
|
||||
|
||||
# Handle Brightness
|
||||
if CHAR_BRIGHTNESS in self.chars:
|
||||
brightness = new_state.attributes.get(ATTR_BRIGHTNESS)
|
||||
if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int):
|
||||
if isinstance(brightness, (int, float)):
|
||||
brightness = round(brightness / 255 * 100, 0)
|
||||
# The homeassistant component might report its brightness as 0 but is
|
||||
# not off. But 0 is a special value in homekit. When you turn on a
|
||||
# homekit accessory it will try to restore the last brightness state
|
||||
# which will be the last value saved by char_brightness.set_value.
|
||||
# But if it is set to 0, HomeKit will update the brightness to 100 as
|
||||
# it thinks 0 is off.
|
||||
#
|
||||
# Therefore, if the the brightness is 0 and the device is still on,
|
||||
# the brightness is mapped to 1 otherwise the update is ignored in
|
||||
# order to avoid this incorrect behavior.
|
||||
if brightness == 0 and state == STATE_ON:
|
||||
brightness = 1
|
||||
if self.char_brightness.value != brightness:
|
||||
# The homeassistant component might report its brightness as 0 but is
|
||||
# not off. But 0 is a special value in homekit. When you turn on a
|
||||
# homekit accessory it will try to restore the last brightness state
|
||||
# which will be the last value saved by char_brightness.set_value.
|
||||
# But if it is set to 0, HomeKit will update the brightness to 100 as
|
||||
# it thinks 0 is off.
|
||||
#
|
||||
# Therefore, if the the brightness is 0 and the device is still on,
|
||||
# the brightness is mapped to 1 otherwise the update is ignored in
|
||||
# order to avoid this incorrect behavior.
|
||||
if brightness == 0:
|
||||
if state == STATE_ON:
|
||||
self.char_brightness.set_value(1)
|
||||
else:
|
||||
self.char_brightness.set_value(brightness)
|
||||
self._flag[CHAR_BRIGHTNESS] = False
|
||||
self.char_brightness.set_value(brightness)
|
||||
|
||||
# Handle color temperature
|
||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||
color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP)
|
||||
if (
|
||||
not self._flag[CHAR_COLOR_TEMPERATURE]
|
||||
and isinstance(color_temperature, int)
|
||||
and self.char_color_temperature.value != color_temperature
|
||||
):
|
||||
self.char_color_temperature.set_value(color_temperature)
|
||||
self._flag[CHAR_COLOR_TEMPERATURE] = False
|
||||
if isinstance(color_temperature, (int, float)):
|
||||
color_temperature = round(color_temperature, 0)
|
||||
if self.char_color_temperature.value != color_temperature:
|
||||
self.char_color_temperature.set_value(color_temperature)
|
||||
|
||||
# Handle Color
|
||||
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
|
||||
hue, saturation = new_state.attributes.get(ATTR_HS_COLOR, (None, None))
|
||||
if (
|
||||
not self._flag[RGB_COLOR]
|
||||
and (hue != self._hue or saturation != self._saturation)
|
||||
and isinstance(hue, (int, float))
|
||||
and isinstance(saturation, (int, float))
|
||||
):
|
||||
self.char_hue.set_value(hue)
|
||||
self.char_saturation.set_value(saturation)
|
||||
self._hue, self._saturation = (hue, saturation)
|
||||
self._flag[RGB_COLOR] = False
|
||||
if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)):
|
||||
hue = round(hue, 0)
|
||||
saturation = round(saturation, 0)
|
||||
if hue != self.char_hue.value:
|
||||
self.char_hue.set_value(hue)
|
||||
if saturation != self.char_saturation.value:
|
||||
self.char_saturation.set_value(saturation)
|
||||
|
||||
@@ -85,7 +85,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self.discovery_info.update(
|
||||
{
|
||||
CONF_HOST: host,
|
||||
CONF_HOST: discovery_info[CONF_HOST],
|
||||
CONF_PORT: port,
|
||||
CONF_SSL: tls,
|
||||
CONF_VERIFY_SSL: False,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Luftdaten",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/luftdaten",
|
||||
"requirements": ["luftdaten==0.6.3"],
|
||||
"requirements": ["luftdaten==0.6.4"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@fabaff"],
|
||||
"quality_scale": "gold"
|
||||
|
||||
@@ -122,10 +122,10 @@ class MjpegCamera(Camera):
|
||||
return image
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error("Timeout getting camera image")
|
||||
_LOGGER.error("Timeout getting camera image from %s", self._name)
|
||||
|
||||
except aiohttp.ClientError as err:
|
||||
_LOGGER.error("Error getting new camera image: %s", err)
|
||||
_LOGGER.error("Error getting new camera image from %s: %s", self._name, err)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
|
||||
@@ -9,6 +9,7 @@ DEFAULT_PORT = 32400
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_VERIFY_SSL = True
|
||||
|
||||
DEBOUNCE_TIMEOUT = 1
|
||||
DISPATCHERS = "dispatchers"
|
||||
PLATFORMS = frozenset(["media_player", "sensor"])
|
||||
PLATFORMS_COMPLETED = "platforms_completed"
|
||||
@@ -38,3 +39,6 @@ X_PLEX_DEVICE_NAME = "Home Assistant"
|
||||
X_PLEX_PLATFORM = "Home Assistant"
|
||||
X_PLEX_PRODUCT = "Home Assistant"
|
||||
X_PLEX_VERSION = __version__
|
||||
|
||||
COMMAND_MEDIA_TYPE_MUSIC = "music"
|
||||
COMMAND_MEDIA_TYPE_VIDEO = "video"
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.media_player.const import (
|
||||
MEDIA_TYPE_MOVIE,
|
||||
MEDIA_TYPE_MUSIC,
|
||||
MEDIA_TYPE_TVSHOW,
|
||||
MEDIA_TYPE_VIDEO,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY,
|
||||
@@ -28,6 +27,8 @@ from homeassistant.helpers.entity_registry import async_get_registry
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
COMMAND_MEDIA_TYPE_MUSIC,
|
||||
COMMAND_MEDIA_TYPE_VIDEO,
|
||||
COMMON_PLAYERS,
|
||||
CONF_SERVER_IDENTIFIER,
|
||||
DISPATCHERS,
|
||||
@@ -576,11 +577,11 @@ class PlexMediaPlayer(MediaPlayerDevice):
|
||||
shuffle = src.get("shuffle", 0)
|
||||
|
||||
media = None
|
||||
command_media_type = MEDIA_TYPE_VIDEO
|
||||
command_media_type = COMMAND_MEDIA_TYPE_VIDEO
|
||||
|
||||
if media_type == "MUSIC":
|
||||
media = self._get_music_media(library, src)
|
||||
command_media_type = MEDIA_TYPE_MUSIC
|
||||
command_media_type = COMMAND_MEDIA_TYPE_MUSIC
|
||||
elif media_type == "EPISODE":
|
||||
media = self._get_tv_media(library, src)
|
||||
elif media_type == "PLAYLIST":
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Shared class to maintain Plex server instances."""
|
||||
from functools import partial, wraps
|
||||
import logging
|
||||
import ssl
|
||||
from urllib.parse import urlparse
|
||||
@@ -12,6 +13,7 @@ import requests.exceptions
|
||||
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||
from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .const import (
|
||||
CONF_CLIENT_IDENTIFIER,
|
||||
@@ -19,6 +21,7 @@ from .const import (
|
||||
CONF_MONITORED_USERS,
|
||||
CONF_SERVER,
|
||||
CONF_USE_EPISODE_ART,
|
||||
DEBOUNCE_TIMEOUT,
|
||||
DEFAULT_VERIFY_SSL,
|
||||
PLEX_NEW_MP_SIGNAL,
|
||||
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL,
|
||||
@@ -39,12 +42,37 @@ plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT
|
||||
plexapi.X_PLEX_VERSION = X_PLEX_VERSION
|
||||
|
||||
|
||||
def debounce(func):
|
||||
"""Decorate function to debounce callbacks from Plex websocket."""
|
||||
|
||||
unsub = None
|
||||
|
||||
async def call_later_listener(self, _):
|
||||
"""Handle call_later callback."""
|
||||
nonlocal unsub
|
||||
unsub = None
|
||||
await self.hass.async_add_executor_job(func, self)
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self):
|
||||
"""Schedule async callback."""
|
||||
nonlocal unsub
|
||||
if unsub:
|
||||
_LOGGER.debug("Throttling update of %s", self.friendly_name)
|
||||
unsub() # pylint: disable=not-callable
|
||||
unsub = async_call_later(
|
||||
self.hass, DEBOUNCE_TIMEOUT, partial(call_later_listener, self),
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class PlexServer:
|
||||
"""Manages a single Plex server connection."""
|
||||
|
||||
def __init__(self, hass, server_config, known_server_id=None, options=None):
|
||||
"""Initialize a Plex server instance."""
|
||||
self._hass = hass
|
||||
self.hass = hass
|
||||
self._plex_server = None
|
||||
self._known_clients = set()
|
||||
self._known_idle = set()
|
||||
@@ -131,6 +159,7 @@ class PlexServer:
|
||||
for account in self._plex_server.systemAccounts()
|
||||
if account.name
|
||||
]
|
||||
_LOGGER.debug("Linked accounts: %s", self.accounts)
|
||||
|
||||
owner_account = [
|
||||
account.name
|
||||
@@ -139,6 +168,7 @@ class PlexServer:
|
||||
]
|
||||
if owner_account:
|
||||
self._owner_username = owner_account[0]
|
||||
_LOGGER.debug("Server owner found: '%s'", self._owner_username)
|
||||
|
||||
self._version = self._plex_server.version
|
||||
|
||||
@@ -150,12 +180,13 @@ class PlexServer:
|
||||
unique_id = f"{self.machine_identifier}:{machine_identifier}"
|
||||
_LOGGER.debug("Refreshing %s", unique_id)
|
||||
dispatcher_send(
|
||||
self._hass,
|
||||
self.hass,
|
||||
PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id),
|
||||
device,
|
||||
session,
|
||||
)
|
||||
|
||||
@debounce
|
||||
def update_platforms(self):
|
||||
"""Update the platform entities."""
|
||||
_LOGGER.debug("Updating devices")
|
||||
@@ -180,11 +211,11 @@ class PlexServer:
|
||||
try:
|
||||
devices = self._plex_server.clients()
|
||||
sessions = self._plex_server.sessions()
|
||||
except plexapi.exceptions.BadRequest:
|
||||
_LOGGER.exception("Error requesting Plex client data from server")
|
||||
return
|
||||
except requests.exceptions.RequestException as ex:
|
||||
_LOGGER.warning(
|
||||
except (
|
||||
plexapi.exceptions.BadRequest,
|
||||
requests.exceptions.RequestException,
|
||||
) as ex:
|
||||
_LOGGER.error(
|
||||
"Could not connect to Plex server: %s (%s)", self.friendly_name, ex
|
||||
)
|
||||
return
|
||||
@@ -205,7 +236,9 @@ class PlexServer:
|
||||
for player in session.players:
|
||||
if session_username and session_username not in monitored_users:
|
||||
ignored_clients.add(player.machineIdentifier)
|
||||
_LOGGER.debug("Ignoring Plex client owned by %s", session_username)
|
||||
_LOGGER.debug(
|
||||
"Ignoring Plex client owned by '%s'", session_username
|
||||
)
|
||||
continue
|
||||
self._known_idle.discard(player.machineIdentifier)
|
||||
available_clients.setdefault(
|
||||
@@ -239,13 +272,13 @@ class PlexServer:
|
||||
|
||||
if new_entity_configs:
|
||||
dispatcher_send(
|
||||
self._hass,
|
||||
self.hass,
|
||||
PLEX_NEW_MP_SIGNAL.format(self.machine_identifier),
|
||||
new_entity_configs,
|
||||
)
|
||||
|
||||
dispatcher_send(
|
||||
self._hass,
|
||||
self.hass,
|
||||
PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier),
|
||||
sessions,
|
||||
)
|
||||
|
||||
@@ -614,11 +614,11 @@ class SonosEntity(MediaPlayerDevice):
|
||||
except (TypeError, KeyError, AttributeError):
|
||||
pass
|
||||
|
||||
# Radios without tagging can have the radio URI as title. Non-playing
|
||||
# radios will not have a current title. In these cases we try to use
|
||||
# the radio name instead.
|
||||
# Radios without tagging can have part of the radio URI as title.
|
||||
# Non-playing radios will not have a current title. In these cases we
|
||||
# try to use the radio name instead.
|
||||
try:
|
||||
if self.soco.is_radio_uri(self._media_title) or self.state != STATE_PLAYING:
|
||||
if self._media_title in self._uri or self.state != STATE_PLAYING:
|
||||
self._media_title = variables["enqueued_transport_uri_meta_data"].title
|
||||
except (TypeError, KeyError, AttributeError):
|
||||
pass
|
||||
|
||||
@@ -133,7 +133,7 @@ async def async_setup(hass, config):
|
||||
hass, p_config, discovery_info
|
||||
)
|
||||
else:
|
||||
provider = await hass.async_add_job(
|
||||
provider = await hass.async_add_executor_job(
|
||||
platform.get_engine, hass, p_config, discovery_info
|
||||
)
|
||||
|
||||
@@ -226,41 +226,17 @@ class SpeechManager:
|
||||
self.time_memory = time_memory
|
||||
self.base_url = base_url
|
||||
|
||||
def init_tts_cache_dir(cache_dir):
|
||||
"""Init cache folder."""
|
||||
if not os.path.isabs(cache_dir):
|
||||
cache_dir = self.hass.config.path(cache_dir)
|
||||
if not os.path.isdir(cache_dir):
|
||||
_LOGGER.info("Create cache dir %s.", cache_dir)
|
||||
os.mkdir(cache_dir)
|
||||
return cache_dir
|
||||
|
||||
try:
|
||||
self.cache_dir = await self.hass.async_add_job(
|
||||
init_tts_cache_dir, cache_dir
|
||||
self.cache_dir = await self.hass.async_add_executor_job(
|
||||
_init_tts_cache_dir, self.hass, cache_dir
|
||||
)
|
||||
except OSError as err:
|
||||
raise HomeAssistantError(f"Can't init cache dir {err}")
|
||||
|
||||
def get_cache_files():
|
||||
"""Return a dict of given engine files."""
|
||||
cache = {}
|
||||
|
||||
folder_data = os.listdir(self.cache_dir)
|
||||
for file_data in folder_data:
|
||||
record = _RE_VOICE_FILE.match(file_data)
|
||||
if record:
|
||||
key = KEY_PATTERN.format(
|
||||
record.group(1),
|
||||
record.group(2),
|
||||
record.group(3),
|
||||
record.group(4),
|
||||
)
|
||||
cache[key.lower()] = file_data.lower()
|
||||
return cache
|
||||
|
||||
try:
|
||||
cache_files = await self.hass.async_add_job(get_cache_files)
|
||||
cache_files = await self.hass.async_add_executor_job(
|
||||
_get_cache_files, self.cache_dir
|
||||
)
|
||||
except OSError as err:
|
||||
raise HomeAssistantError(f"Can't read cache dir {err}")
|
||||
|
||||
@@ -273,13 +249,13 @@ class SpeechManager:
|
||||
|
||||
def remove_files():
|
||||
"""Remove files from filesystem."""
|
||||
for _, filename in self.file_cache.items():
|
||||
for filename in self.file_cache.values():
|
||||
try:
|
||||
os.remove(os.path.join(self.cache_dir, filename))
|
||||
except OSError as err:
|
||||
_LOGGER.warning("Can't remove cache file '%s': %s", filename, err)
|
||||
|
||||
await self.hass.async_add_job(remove_files)
|
||||
await self.hass.async_add_executor_job(remove_files)
|
||||
self.file_cache = {}
|
||||
|
||||
@callback
|
||||
@@ -312,6 +288,7 @@ class SpeechManager:
|
||||
merged_options.update(options)
|
||||
options = merged_options
|
||||
options = options or provider.default_options
|
||||
|
||||
if options is not None:
|
||||
invalid_opts = [
|
||||
opt_name
|
||||
@@ -378,10 +355,10 @@ class SpeechManager:
|
||||
speech.write(data)
|
||||
|
||||
try:
|
||||
await self.hass.async_add_job(save_speech)
|
||||
await self.hass.async_add_executor_job(save_speech)
|
||||
self.file_cache[key] = filename
|
||||
except OSError:
|
||||
_LOGGER.error("Can't write %s", filename)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't write %s: %s", filename, err)
|
||||
|
||||
async def async_file_to_mem(self, key):
|
||||
"""Load voice from file cache into memory.
|
||||
@@ -400,7 +377,7 @@ class SpeechManager:
|
||||
return speech.read()
|
||||
|
||||
try:
|
||||
data = await self.hass.async_add_job(load_speech)
|
||||
data = await self.hass.async_add_executor_job(load_speech)
|
||||
except OSError:
|
||||
del self.file_cache[key]
|
||||
raise HomeAssistantError(f"Can't read {voice_file}")
|
||||
@@ -506,11 +483,36 @@ class Provider:
|
||||
|
||||
Return a tuple of file extension and data as bytes.
|
||||
"""
|
||||
return await self.hass.async_add_job(
|
||||
return await self.hass.async_add_executor_job(
|
||||
ft.partial(self.get_tts_audio, message, language, options=options)
|
||||
)
|
||||
|
||||
|
||||
def _init_tts_cache_dir(hass, cache_dir):
|
||||
"""Init cache folder."""
|
||||
if not os.path.isabs(cache_dir):
|
||||
cache_dir = hass.config.path(cache_dir)
|
||||
if not os.path.isdir(cache_dir):
|
||||
_LOGGER.info("Create cache dir %s", cache_dir)
|
||||
os.mkdir(cache_dir)
|
||||
return cache_dir
|
||||
|
||||
|
||||
def _get_cache_files(cache_dir):
|
||||
"""Return a dict of given engine files."""
|
||||
cache = {}
|
||||
|
||||
folder_data = os.listdir(cache_dir)
|
||||
for file_data in folder_data:
|
||||
record = _RE_VOICE_FILE.match(file_data)
|
||||
if record:
|
||||
key = KEY_PATTERN.format(
|
||||
record.group(1), record.group(2), record.group(3), record.group(4),
|
||||
)
|
||||
cache[key.lower()] = file_data.lower()
|
||||
return cache
|
||||
|
||||
|
||||
class TextToSpeechUrlView(HomeAssistantView):
|
||||
"""TTS view to get a url to a generated speech file."""
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"name": "Twente Milieu",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/twentemilieu",
|
||||
"requirements": ["twentemilieu==0.2.0"],
|
||||
"dependencies": [],
|
||||
"requirements": ["twentemilieu==0.3.0"],
|
||||
"codeowners": ["@frenck"]
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context.update(
|
||||
{
|
||||
CONF_HOST: host,
|
||||
CONF_HOST: user_input["host"],
|
||||
CONF_NAME: name,
|
||||
CONF_MAC: user_input["properties"].get(CONF_MAC),
|
||||
"title_placeholders": {"name": name},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "zeroconf",
|
||||
"name": "Zero-configuration networking (zeroconf)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
|
||||
"requirements": ["zeroconf==0.24.5"],
|
||||
"requirements": ["zeroconf==0.25.0"],
|
||||
"dependencies": ["api"],
|
||||
"codeowners": ["@robbiet480", "@Kane610"],
|
||||
"quality_scale": "internal"
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zha",
|
||||
"requirements": [
|
||||
"bellows-homeassistant==0.15.1",
|
||||
"bellows-homeassistant==0.15.2",
|
||||
"zha-quirks==0.0.38",
|
||||
"zigpy-cc==0.3.1",
|
||||
"zigpy-deconz==0.8.0",
|
||||
"zigpy-homeassistant==0.18.0",
|
||||
"zigpy-homeassistant==0.18.1",
|
||||
"zigpy-xbee-homeassistant==0.11.0",
|
||||
"zigpy-zigate==0.5.1"
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 108
|
||||
PATCH_VERSION = "0b1"
|
||||
PATCH_VERSION = "0b3"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 7, 0)
|
||||
|
||||
@@ -12,7 +12,7 @@ cryptography==2.8
|
||||
defusedxml==0.6.0
|
||||
distro==1.4.0
|
||||
hass-nabucasa==0.32.2
|
||||
home-assistant-frontend==20200401.0
|
||||
home-assistant-frontend==20200403.0
|
||||
importlib-metadata==1.5.0
|
||||
jinja2>=2.11.1
|
||||
netdisco==2.6.0
|
||||
@@ -25,7 +25,7 @@ ruamel.yaml==0.15.100
|
||||
sqlalchemy==1.3.15
|
||||
voluptuous-serialize==2.3.0
|
||||
voluptuous==0.11.7
|
||||
zeroconf==0.24.5
|
||||
zeroconf==0.25.0
|
||||
|
||||
pycryptodome>=3.6.6
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ adafruit-circuitpython-mcp230xx==2.2.2
|
||||
adb-shell==0.1.1
|
||||
|
||||
# homeassistant.components.adguard
|
||||
adguardhome==0.4.1
|
||||
adguardhome==0.4.2
|
||||
|
||||
# homeassistant.components.frontier_silicon
|
||||
afsapi==0.0.4
|
||||
@@ -317,7 +317,7 @@ beautifulsoup4==4.8.2
|
||||
beewi_smartclim==0.0.7
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows-homeassistant==0.15.1
|
||||
bellows-homeassistant==0.15.2
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.7.1
|
||||
@@ -611,7 +611,7 @@ georss_qld_bushfire_alert_client==0.3
|
||||
getmac==0.8.1
|
||||
|
||||
# homeassistant.components.gios
|
||||
gios==0.0.5
|
||||
gios==0.1.1
|
||||
|
||||
# homeassistant.components.gitter
|
||||
gitterpy==0.1.7
|
||||
@@ -704,7 +704,7 @@ hole==0.5.1
|
||||
holidays==0.10.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20200401.0
|
||||
home-assistant-frontend==20200403.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -840,7 +840,7 @@ logi_circle==0.2.2
|
||||
london-tube-status==0.2
|
||||
|
||||
# homeassistant.components.luftdaten
|
||||
luftdaten==0.6.3
|
||||
luftdaten==0.6.4
|
||||
|
||||
# homeassistant.components.lupusec
|
||||
lupupy==0.0.18
|
||||
@@ -2047,7 +2047,7 @@ transmissionrpc==0.11
|
||||
tuyaha==0.0.5
|
||||
|
||||
# homeassistant.components.twentemilieu
|
||||
twentemilieu==0.2.0
|
||||
twentemilieu==0.3.0
|
||||
|
||||
# homeassistant.components.twilio
|
||||
twilio==6.32.0
|
||||
@@ -2173,7 +2173,7 @@ youtube_dl==2020.03.24
|
||||
zengge==0.2
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.24.5
|
||||
zeroconf==0.25.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.38
|
||||
@@ -2191,7 +2191,7 @@ zigpy-cc==0.3.1
|
||||
zigpy-deconz==0.8.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-homeassistant==0.18.0
|
||||
zigpy-homeassistant==0.18.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-xbee-homeassistant==0.11.0
|
||||
|
||||
@@ -32,7 +32,7 @@ abodepy==0.18.1
|
||||
adb-shell==0.1.1
|
||||
|
||||
# homeassistant.components.adguard
|
||||
adguardhome==0.4.1
|
||||
adguardhome==0.4.2
|
||||
|
||||
# homeassistant.components.geonetnz_quakes
|
||||
aio_geojson_geonetnz_quakes==0.12
|
||||
@@ -131,7 +131,7 @@ av==6.1.2
|
||||
axis==25
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows-homeassistant==0.15.1
|
||||
bellows-homeassistant==0.15.2
|
||||
|
||||
# homeassistant.components.bom
|
||||
bomradarloop==0.1.4
|
||||
@@ -243,7 +243,7 @@ georss_qld_bushfire_alert_client==0.3
|
||||
getmac==0.8.1
|
||||
|
||||
# homeassistant.components.gios
|
||||
gios==0.0.5
|
||||
gios==0.1.1
|
||||
|
||||
# homeassistant.components.glances
|
||||
glances_api==0.2.0
|
||||
@@ -282,7 +282,7 @@ hole==0.5.1
|
||||
holidays==0.10.1
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20200401.0
|
||||
home-assistant-frontend==20200403.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -329,7 +329,7 @@ libsoundtouch==0.7.2
|
||||
logi_circle==0.2.2
|
||||
|
||||
# homeassistant.components.luftdaten
|
||||
luftdaten==0.6.3
|
||||
luftdaten==0.6.4
|
||||
|
||||
# homeassistant.components.mythicbeastsdns
|
||||
mbddns==0.1.2
|
||||
@@ -744,7 +744,7 @@ toonapilib==3.2.4
|
||||
transmissionrpc==0.11
|
||||
|
||||
# homeassistant.components.twentemilieu
|
||||
twentemilieu==0.2.0
|
||||
twentemilieu==0.3.0
|
||||
|
||||
# homeassistant.components.twilio
|
||||
twilio==6.32.0
|
||||
@@ -795,7 +795,7 @@ ya_ma==0.3.8
|
||||
yahooweather==0.10
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.24.5
|
||||
zeroconf==0.25.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.38
|
||||
@@ -807,7 +807,7 @@ zigpy-cc==0.3.1
|
||||
zigpy-deconz==0.8.0
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-homeassistant==0.18.0
|
||||
zigpy-homeassistant==0.18.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-xbee-homeassistant==0.11.0
|
||||
|
||||
@@ -14,6 +14,8 @@ import threading
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
import uuid
|
||||
|
||||
from aiohttp.test_utils import unused_port as get_test_instance_port # noqa
|
||||
|
||||
from homeassistant import auth, config_entries, core as ha, loader
|
||||
from homeassistant.auth import (
|
||||
auth_store,
|
||||
@@ -37,7 +39,6 @@ from homeassistant.const import (
|
||||
EVENT_PLATFORM_DISCOVERED,
|
||||
EVENT_STATE_CHANGED,
|
||||
EVENT_TIME_CHANGED,
|
||||
SERVER_PORT,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
@@ -59,7 +60,6 @@ import homeassistant.util.dt as date_util
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
import homeassistant.util.yaml.loader as yaml_loader
|
||||
|
||||
_TEST_INSTANCE_PORT = SERVER_PORT
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
INSTANCES = []
|
||||
CLIENT_ID = "https://example.com/app"
|
||||
@@ -217,18 +217,6 @@ async def async_test_home_assistant(loop):
|
||||
return hass
|
||||
|
||||
|
||||
def get_test_instance_port():
|
||||
"""Return unused port for running test instance.
|
||||
|
||||
The socket that holds the default port does not get released when we stop
|
||||
HA in a different test case. Until I have figured out what is going on,
|
||||
let's run each test on a different port.
|
||||
"""
|
||||
global _TEST_INSTANCE_PORT
|
||||
_TEST_INSTANCE_PORT += 1
|
||||
return _TEST_INSTANCE_PORT
|
||||
|
||||
|
||||
def async_mock_service(hass, domain, service, schema=None):
|
||||
"""Set up a fake service & return a calls log list to this service."""
|
||||
calls = []
|
||||
|
||||
@@ -24,6 +24,18 @@ async def test_password_or_pub_key_required(hass):
|
||||
assert not result
|
||||
|
||||
|
||||
async def test_network_unreachable(hass):
|
||||
"""Test creating an AsusWRT scanner without a pass or pubkey."""
|
||||
with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt:
|
||||
AsusWrt().connection.async_connect = mock_coro_func(exception=OSError)
|
||||
AsusWrt().is_connected = False
|
||||
result = await async_setup_component(
|
||||
hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}}
|
||||
)
|
||||
assert result
|
||||
assert hass.data.get(DATA_ASUSWRT, None) is None
|
||||
|
||||
|
||||
async def test_get_scanner_with_password_no_pubkey(hass):
|
||||
"""Test creating an AsusWRT scanner with a password and no pubkey."""
|
||||
with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"""Test different accessory types: Lights."""
|
||||
from collections import namedtuple
|
||||
|
||||
from asynctest import patch
|
||||
from pyhap.accessory_driver import AccessoryDriver
|
||||
from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homekit.const import ATTR_VALUE
|
||||
@@ -30,6 +33,15 @@ from tests.common import async_mock_service
|
||||
from tests.components.homekit.common import patch_debounce
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def driver():
|
||||
"""Patch AccessoryDriver without zeroconf or HAPServer."""
|
||||
with patch("pyhap.accessory_driver.HAPServer"), patch(
|
||||
"pyhap.accessory_driver.Zeroconf"
|
||||
), patch("pyhap.accessory_driver.AccessoryDriver.persist"):
|
||||
yield AccessoryDriver()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def cls():
|
||||
"""Patch debounce decorator during import of type_lights."""
|
||||
@@ -43,15 +55,16 @@ def cls():
|
||||
patcher.stop()
|
||||
|
||||
|
||||
async def test_light_basic(hass, hk_driver, cls, events):
|
||||
async def test_light_basic(hass, hk_driver, cls, events, driver):
|
||||
"""Test light with char state."""
|
||||
entity_id = "light.demo"
|
||||
|
||||
hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0})
|
||||
await hass.async_block_till_done()
|
||||
acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None)
|
||||
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
|
||||
driver.add_accessory(acc)
|
||||
|
||||
assert acc.aid == 2
|
||||
assert acc.aid == 1
|
||||
assert acc.category == 5 # Lightbulb
|
||||
assert acc.char_on.value == 0
|
||||
|
||||
@@ -75,25 +88,43 @@ async def test_light_basic(hass, hk_driver, cls, events):
|
||||
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
|
||||
call_turn_off = async_mock_service(hass, DOMAIN, "turn_off")
|
||||
|
||||
char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID]
|
||||
|
||||
driver.set_characteristics(
|
||||
{
|
||||
HAP_REPR_CHARS: [
|
||||
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}
|
||||
]
|
||||
},
|
||||
"mock_addr",
|
||||
)
|
||||
|
||||
await hass.async_add_job(acc.char_on.client_update_value, 1)
|
||||
await hass.async_block_till_done()
|
||||
assert call_turn_on
|
||||
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
|
||||
assert len(events) == 1
|
||||
assert events[-1].data[ATTR_VALUE] is None
|
||||
assert events[-1].data[ATTR_VALUE] == "Set state to 1"
|
||||
|
||||
hass.states.async_set(entity_id, STATE_ON)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.async_add_job(acc.char_on.client_update_value, 0)
|
||||
driver.set_characteristics(
|
||||
{
|
||||
HAP_REPR_CHARS: [
|
||||
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 0}
|
||||
]
|
||||
},
|
||||
"mock_addr",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert call_turn_off
|
||||
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
|
||||
assert len(events) == 2
|
||||
assert events[-1].data[ATTR_VALUE] is None
|
||||
assert events[-1].data[ATTR_VALUE] == "Set state to 0"
|
||||
|
||||
|
||||
async def test_light_brightness(hass, hk_driver, cls, events):
|
||||
async def test_light_brightness(hass, hk_driver, cls, events, driver):
|
||||
"""Test light with brightness."""
|
||||
entity_id = "light.demo"
|
||||
|
||||
@@ -103,11 +134,14 @@ async def test_light_brightness(hass, hk_driver, cls, events):
|
||||
{ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None)
|
||||
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
|
||||
driver.add_accessory(acc)
|
||||
|
||||
# Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the
|
||||
# brightness to 100 when turning on a light on a freshly booted up server.
|
||||
assert acc.char_brightness.value != 0
|
||||
char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID]
|
||||
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
|
||||
|
||||
await hass.async_add_job(acc.run)
|
||||
await hass.async_block_till_done()
|
||||
@@ -121,34 +155,99 @@ async def test_light_brightness(hass, hk_driver, cls, events):
|
||||
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
|
||||
call_turn_off = async_mock_service(hass, DOMAIN, "turn_off")
|
||||
|
||||
await hass.async_add_job(acc.char_brightness.client_update_value, 20)
|
||||
await hass.async_add_job(acc.char_on.client_update_value, 1)
|
||||
driver.set_characteristics(
|
||||
{
|
||||
HAP_REPR_CHARS: [
|
||||
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1},
|
||||
{
|
||||
HAP_REPR_AID: acc.aid,
|
||||
HAP_REPR_IID: char_brightness_iid,
|
||||
HAP_REPR_VALUE: 20,
|
||||
},
|
||||
]
|
||||
},
|
||||
"mock_addr",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert call_turn_on[0]
|
||||
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
|
||||
assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20
|
||||
assert len(events) == 1
|
||||
assert events[-1].data[ATTR_VALUE] == f"brightness at 20{UNIT_PERCENTAGE}"
|
||||
assert (
|
||||
events[-1].data[ATTR_VALUE]
|
||||
== f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}"
|
||||
)
|
||||
|
||||
await hass.async_add_job(acc.char_on.client_update_value, 1)
|
||||
await hass.async_add_job(acc.char_brightness.client_update_value, 40)
|
||||
driver.set_characteristics(
|
||||
{
|
||||
HAP_REPR_CHARS: [
|
||||
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1},
|
||||
{
|
||||
HAP_REPR_AID: acc.aid,
|
||||
HAP_REPR_IID: char_brightness_iid,
|
||||
HAP_REPR_VALUE: 40,
|
||||
},
|
||||
]
|
||||
},
|
||||
"mock_addr",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert call_turn_on[1]
|
||||
assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id
|
||||
assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40
|
||||
assert len(events) == 2
|
||||
assert events[-1].data[ATTR_VALUE] == f"brightness at 40{UNIT_PERCENTAGE}"
|
||||
assert (
|
||||
events[-1].data[ATTR_VALUE]
|
||||
== f"Set state to 1, brightness at 40{UNIT_PERCENTAGE}"
|
||||
)
|
||||
|
||||
await hass.async_add_job(acc.char_on.client_update_value, 1)
|
||||
await hass.async_add_job(acc.char_brightness.client_update_value, 0)
|
||||
driver.set_characteristics(
|
||||
{
|
||||
HAP_REPR_CHARS: [
|
||||
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1},
|
||||
{
|
||||
HAP_REPR_AID: acc.aid,
|
||||
HAP_REPR_IID: char_brightness_iid,
|
||||
HAP_REPR_VALUE: 0,
|
||||
},
|
||||
]
|
||||
},
|
||||
"mock_addr",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert call_turn_off
|
||||
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
|
||||
assert len(events) == 3
|
||||
assert events[-1].data[ATTR_VALUE] is None
|
||||
assert (
|
||||
events[-1].data[ATTR_VALUE]
|
||||
== f"Set state to 0, brightness at 0{UNIT_PERCENTAGE}"
|
||||
)
|
||||
|
||||
# 0 is a special case for homekit, see "Handle Brightness"
|
||||
# in update_state
|
||||
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_brightness.value == 1
|
||||
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 255})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_brightness.value == 100
|
||||
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_brightness.value == 1
|
||||
|
||||
# Ensure floats are handled
|
||||
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 55.66})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_brightness.value == 22
|
||||
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 108.4})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_brightness.value == 43
|
||||
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0.0})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_brightness.value == 1
|
||||
|
||||
|
||||
async def test_light_color_temperature(hass, hk_driver, cls, events):
|
||||
async def test_light_color_temperature(hass, hk_driver, cls, events, driver):
|
||||
"""Test light with color temperature."""
|
||||
entity_id = "light.demo"
|
||||
|
||||
@@ -158,7 +257,8 @@ async def test_light_color_temperature(hass, hk_driver, cls, events):
|
||||
{ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None)
|
||||
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
|
||||
driver.add_accessory(acc)
|
||||
|
||||
assert acc.char_color_temperature.value == 153
|
||||
|
||||
@@ -169,6 +269,20 @@ async def test_light_color_temperature(hass, hk_driver, cls, events):
|
||||
# Set from HomeKit
|
||||
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
|
||||
|
||||
char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID]
|
||||
|
||||
driver.set_characteristics(
|
||||
{
|
||||
HAP_REPR_CHARS: [
|
||||
{
|
||||
HAP_REPR_AID: acc.aid,
|
||||
HAP_REPR_IID: char_color_temperature_iid,
|
||||
HAP_REPR_VALUE: 250,
|
||||
}
|
||||
]
|
||||
},
|
||||
"mock_addr",
|
||||
)
|
||||
await hass.async_add_job(acc.char_color_temperature.client_update_value, 250)
|
||||
await hass.async_block_till_done()
|
||||
assert call_turn_on
|
||||
@@ -197,7 +311,7 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, event
|
||||
assert not hasattr(acc, "char_color_temperature")
|
||||
|
||||
|
||||
async def test_light_rgb_color(hass, hk_driver, cls, events):
|
||||
async def test_light_rgb_color(hass, hk_driver, cls, events, driver):
|
||||
"""Test light with rgb_color."""
|
||||
entity_id = "light.demo"
|
||||
|
||||
@@ -207,7 +321,8 @@ async def test_light_rgb_color(hass, hk_driver, cls, events):
|
||||
{ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None)
|
||||
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
|
||||
driver.add_accessory(acc)
|
||||
|
||||
assert acc.char_hue.value == 0
|
||||
assert acc.char_saturation.value == 75
|
||||
@@ -220,8 +335,26 @@ async def test_light_rgb_color(hass, hk_driver, cls, events):
|
||||
# Set from HomeKit
|
||||
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
|
||||
|
||||
await hass.async_add_job(acc.char_hue.client_update_value, 145)
|
||||
await hass.async_add_job(acc.char_saturation.client_update_value, 75)
|
||||
char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID]
|
||||
char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID]
|
||||
|
||||
driver.set_characteristics(
|
||||
{
|
||||
HAP_REPR_CHARS: [
|
||||
{
|
||||
HAP_REPR_AID: acc.aid,
|
||||
HAP_REPR_IID: char_hue_iid,
|
||||
HAP_REPR_VALUE: 145,
|
||||
},
|
||||
{
|
||||
HAP_REPR_AID: acc.aid,
|
||||
HAP_REPR_IID: char_saturation_iid,
|
||||
HAP_REPR_VALUE: 75,
|
||||
},
|
||||
]
|
||||
},
|
||||
"mock_addr",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert call_turn_on
|
||||
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
|
||||
@@ -230,7 +363,7 @@ async def test_light_rgb_color(hass, hk_driver, cls, events):
|
||||
assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)"
|
||||
|
||||
|
||||
async def test_light_restore(hass, hk_driver, cls, events):
|
||||
async def test_light_restore(hass, hk_driver, cls, events, driver):
|
||||
"""Test setting up an entity from state in the event registry."""
|
||||
hass.state = CoreState.not_running
|
||||
|
||||
@@ -250,7 +383,9 @@ async def test_light_restore(hass, hk_driver, cls, events):
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
acc = cls.light(hass, hk_driver, "Light", "light.simple", 2, None)
|
||||
acc = cls.light(hass, hk_driver, "Light", "light.simple", 1, None)
|
||||
driver.add_accessory(acc)
|
||||
|
||||
assert acc.category == 5 # Lightbulb
|
||||
assert acc.chars == []
|
||||
assert acc.char_on.value == 0
|
||||
@@ -259,3 +394,150 @@ async def test_light_restore(hass, hk_driver, cls, events):
|
||||
assert acc.category == 5 # Lightbulb
|
||||
assert acc.chars == ["Brightness"]
|
||||
assert acc.char_on.value == 0
|
||||
|
||||
|
||||
async def test_light_set_brightness_and_color(hass, hk_driver, cls, events, driver):
|
||||
"""Test light with all chars in one go."""
|
||||
entity_id = "light.demo"
|
||||
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_ON,
|
||||
{
|
||||
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR,
|
||||
ATTR_BRIGHTNESS: 255,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
|
||||
driver.add_accessory(acc)
|
||||
|
||||
# Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the
|
||||
# brightness to 100 when turning on a light on a freshly booted up server.
|
||||
assert acc.char_brightness.value != 0
|
||||
char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID]
|
||||
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
|
||||
char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID]
|
||||
char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID]
|
||||
|
||||
await hass.async_add_job(acc.run)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_brightness.value == 100
|
||||
|
||||
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_brightness.value == 40
|
||||
|
||||
hass.states.async_set(entity_id, STATE_ON, {ATTR_HS_COLOR: (4.5, 9.2)})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_hue.value == 4
|
||||
assert acc.char_saturation.value == 9
|
||||
|
||||
# Set from HomeKit
|
||||
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
|
||||
|
||||
driver.set_characteristics(
|
||||
{
|
||||
HAP_REPR_CHARS: [
|
||||
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1},
|
||||
{
|
||||
HAP_REPR_AID: acc.aid,
|
||||
HAP_REPR_IID: char_brightness_iid,
|
||||
HAP_REPR_VALUE: 20,
|
||||
},
|
||||
{
|
||||
HAP_REPR_AID: acc.aid,
|
||||
HAP_REPR_IID: char_hue_iid,
|
||||
HAP_REPR_VALUE: 145,
|
||||
},
|
||||
{
|
||||
HAP_REPR_AID: acc.aid,
|
||||
HAP_REPR_IID: char_saturation_iid,
|
||||
HAP_REPR_VALUE: 75,
|
||||
},
|
||||
]
|
||||
},
|
||||
"mock_addr",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert call_turn_on[0]
|
||||
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
|
||||
assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20
|
||||
assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75)
|
||||
|
||||
assert len(events) == 1
|
||||
assert (
|
||||
events[-1].data[ATTR_VALUE]
|
||||
== f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}, set color at (145, 75)"
|
||||
)
|
||||
|
||||
|
||||
async def test_light_set_brightness_and_color_temp(
|
||||
hass, hk_driver, cls, events, driver
|
||||
):
|
||||
"""Test light with all chars in one go."""
|
||||
entity_id = "light.demo"
|
||||
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_ON,
|
||||
{
|
||||
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP,
|
||||
ATTR_BRIGHTNESS: 255,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None)
|
||||
driver.add_accessory(acc)
|
||||
|
||||
# Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the
|
||||
# brightness to 100 when turning on a light on a freshly booted up server.
|
||||
assert acc.char_brightness.value != 0
|
||||
char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID]
|
||||
char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID]
|
||||
char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID]
|
||||
|
||||
await hass.async_add_job(acc.run)
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_brightness.value == 100
|
||||
|
||||
hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_brightness.value == 40
|
||||
|
||||
hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: (224.14)})
|
||||
await hass.async_block_till_done()
|
||||
assert acc.char_color_temperature.value == 224
|
||||
|
||||
# Set from HomeKit
|
||||
call_turn_on = async_mock_service(hass, DOMAIN, "turn_on")
|
||||
|
||||
driver.set_characteristics(
|
||||
{
|
||||
HAP_REPR_CHARS: [
|
||||
{HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1},
|
||||
{
|
||||
HAP_REPR_AID: acc.aid,
|
||||
HAP_REPR_IID: char_brightness_iid,
|
||||
HAP_REPR_VALUE: 20,
|
||||
},
|
||||
{
|
||||
HAP_REPR_AID: acc.aid,
|
||||
HAP_REPR_IID: char_color_temperature_iid,
|
||||
HAP_REPR_VALUE: 250,
|
||||
},
|
||||
]
|
||||
},
|
||||
"mock_addr",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert call_turn_on[0]
|
||||
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
|
||||
assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20
|
||||
assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250
|
||||
|
||||
assert len(events) == 1
|
||||
assert (
|
||||
events[-1].data[ATTR_VALUE]
|
||||
== f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}, color temperature at 250"
|
||||
)
|
||||
|
||||
@@ -22,13 +22,13 @@ IPP_ZEROCONF_SERVICE_TYPE = "_ipp._tcp.local."
|
||||
IPPS_ZEROCONF_SERVICE_TYPE = "_ipps._tcp.local."
|
||||
|
||||
ZEROCONF_NAME = "EPSON123456"
|
||||
ZEROCONF_HOST = "1.2.3.4"
|
||||
ZEROCONF_HOST = "192.168.1.31"
|
||||
ZEROCONF_HOSTNAME = "EPSON123456.local."
|
||||
ZEROCONF_PORT = 631
|
||||
|
||||
|
||||
MOCK_USER_INPUT = {
|
||||
CONF_HOST: "EPSON123456.local",
|
||||
CONF_HOST: "192.168.1.31",
|
||||
CONF_PORT: 361,
|
||||
CONF_SSL: False,
|
||||
CONF_VERIFY_SSL: False,
|
||||
@@ -37,7 +37,7 @@ MOCK_USER_INPUT = {
|
||||
|
||||
MOCK_ZEROCONF_IPP_SERVICE_INFO = {
|
||||
CONF_TYPE: IPP_ZEROCONF_SERVICE_TYPE,
|
||||
CONF_NAME: ZEROCONF_NAME,
|
||||
CONF_NAME: f"{ZEROCONF_NAME}.{IPP_ZEROCONF_SERVICE_TYPE}",
|
||||
CONF_HOST: ZEROCONF_HOST,
|
||||
ATTR_HOSTNAME: ZEROCONF_HOSTNAME,
|
||||
CONF_PORT: ZEROCONF_PORT,
|
||||
@@ -46,7 +46,7 @@ MOCK_ZEROCONF_IPP_SERVICE_INFO = {
|
||||
|
||||
MOCK_ZEROCONF_IPPS_SERVICE_INFO = {
|
||||
CONF_TYPE: IPPS_ZEROCONF_SERVICE_TYPE,
|
||||
CONF_NAME: ZEROCONF_NAME,
|
||||
CONF_NAME: f"{ZEROCONF_NAME}.{IPPS_ZEROCONF_SERVICE_TYPE}",
|
||||
CONF_HOST: ZEROCONF_HOST,
|
||||
ATTR_HOSTNAME: ZEROCONF_HOSTNAME,
|
||||
CONF_PORT: ZEROCONF_PORT,
|
||||
@@ -65,10 +65,9 @@ async def init_integration(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the IPP integration in Home Assistant."""
|
||||
|
||||
fixture = "ipp/get-printer-attributes.bin"
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print",
|
||||
"http://192.168.1.31:631/ipp/print",
|
||||
content=load_fixture_binary(fixture),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
@@ -77,7 +76,7 @@ async def init_integration(
|
||||
domain=DOMAIN,
|
||||
unique_id="cfe92100-67c4-11d4-a45f-f8d027761251",
|
||||
data={
|
||||
CONF_HOST: "EPSON123456.local",
|
||||
CONF_HOST: "192.168.1.31",
|
||||
CONF_PORT: 631,
|
||||
CONF_SSL: False,
|
||||
CONF_VERIFY_SSL: True,
|
||||
|
||||
@@ -38,7 +38,7 @@ async def test_show_zeroconf_form(
|
||||
) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print",
|
||||
"http://192.168.1.31:631/ipp/print",
|
||||
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
@@ -57,9 +57,7 @@ async def test_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we show user form on IPP connection error."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print", exc=aiohttp.ClientError
|
||||
)
|
||||
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -75,7 +73,7 @@ async def test_zeroconf_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on IPP connection error."""
|
||||
aioclient_mock.post("http://EPSON123456.local/ipp/print", exc=aiohttp.ClientError)
|
||||
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -90,17 +88,11 @@ async def test_zeroconf_confirm_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on IPP connection error."""
|
||||
aioclient_mock.post("http://EPSON123456.local/ipp/print", exc=aiohttp.ClientError)
|
||||
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": SOURCE_ZEROCONF,
|
||||
CONF_HOST: "EPSON123456.local",
|
||||
CONF_NAME: "EPSON123456",
|
||||
},
|
||||
data=discovery_info,
|
||||
DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_ABORT
|
||||
@@ -112,7 +104,7 @@ async def test_user_connection_upgrade_required(
|
||||
) -> None:
|
||||
"""Test we show the user form if connection upgrade required by server."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print", exc=IPPConnectionUpgradeRequired
|
||||
"http://192.168.1.31:631/ipp/print", exc=IPPConnectionUpgradeRequired
|
||||
)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
@@ -130,7 +122,7 @@ async def test_zeroconf_connection_upgrade_required(
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on IPP connection error."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local/ipp/print", exc=IPPConnectionUpgradeRequired
|
||||
"http://192.168.1.31:631/ipp/print", exc=IPPConnectionUpgradeRequired
|
||||
)
|
||||
|
||||
discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy()
|
||||
@@ -193,7 +185,7 @@ async def test_full_user_flow_implementation(
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print",
|
||||
"http://192.168.1.31:631/ipp/print",
|
||||
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
@@ -207,14 +199,14 @@ async def test_full_user_flow_implementation(
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_HOST: "EPSON123456.local", CONF_BASE_PATH: "/ipp/print"},
|
||||
user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"},
|
||||
)
|
||||
|
||||
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "EPSON123456.local"
|
||||
assert result["title"] == "192.168.1.31"
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == "EPSON123456.local"
|
||||
assert result["data"][CONF_HOST] == "192.168.1.31"
|
||||
assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||
|
||||
|
||||
@@ -223,7 +215,7 @@ async def test_full_zeroconf_flow_implementation(
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print",
|
||||
"http://192.168.1.31:631/ipp/print",
|
||||
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
@@ -244,7 +236,7 @@ async def test_full_zeroconf_flow_implementation(
|
||||
assert result["title"] == "EPSON123456"
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == "EPSON123456.local"
|
||||
assert result["data"][CONF_HOST] == "192.168.1.31"
|
||||
assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||
assert not result["data"][CONF_SSL]
|
||||
|
||||
@@ -254,7 +246,7 @@ async def test_full_zeroconf_tls_flow_implementation(
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.post(
|
||||
"https://EPSON123456.local:631/ipp/print",
|
||||
"https://192.168.1.31:631/ipp/print",
|
||||
content=load_fixture_binary("ipp/get-printer-attributes.bin"),
|
||||
headers={"Content-Type": "application/ipp"},
|
||||
)
|
||||
@@ -276,7 +268,7 @@ async def test_full_zeroconf_tls_flow_implementation(
|
||||
assert result["title"] == "EPSON123456"
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == "EPSON123456.local"
|
||||
assert result["data"][CONF_HOST] == "192.168.1.31"
|
||||
assert result["data"][CONF_NAME] == "EPSON123456"
|
||||
assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251"
|
||||
assert result["data"][CONF_SSL]
|
||||
|
||||
@@ -17,9 +17,7 @@ async def test_config_entry_not_ready(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the IPP configuration entry not ready."""
|
||||
aioclient_mock.post(
|
||||
"http://EPSON123456.local:631/ipp/print", exc=aiohttp.ClientError
|
||||
)
|
||||
aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError)
|
||||
|
||||
entry = await init_integration(hass, aioclient_mock)
|
||||
assert entry.state == ENTRY_STATE_SETUP_RETRY
|
||||
|
||||
20
tests/components/plex/common.py
Normal file
20
tests/components/plex/common.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Common fixtures and functions for Plex tests."""
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.plex.const import (
|
||||
DEBOUNCE_TIMEOUT,
|
||||
PLEX_UPDATE_PLATFORMS_SIGNAL,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
async def trigger_plex_update(hass, server_id):
|
||||
"""Update Plex by sending signal and jumping ahead by debounce timeout."""
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
@@ -15,14 +15,13 @@ from homeassistant.components.plex.const import (
|
||||
CONF_USE_EPISODE_ART,
|
||||
DOMAIN,
|
||||
PLEX_SERVER_CONFIG,
|
||||
PLEX_UPDATE_PLATFORMS_SIGNAL,
|
||||
SERVERS,
|
||||
)
|
||||
from homeassistant.config_entries import ENTRY_STATE_LOADED
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import trigger_plex_update
|
||||
from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN
|
||||
from .mock_classes import MockPlexAccount, MockPlexServer
|
||||
|
||||
@@ -416,8 +415,7 @@ async def test_option_flow_new_users_available(hass, caplog):
|
||||
|
||||
server_id = mock_plex_server.machineIdentifier
|
||||
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
await trigger_plex_update(hass, server_id)
|
||||
|
||||
monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ from homeassistant.const import (
|
||||
CONF_URL,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .common import trigger_plex_update
|
||||
from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN
|
||||
from .mock_classes import MockPlexAccount, MockPlexServer
|
||||
|
||||
@@ -74,7 +74,7 @@ async def test_setup_with_config(hass):
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_with_config_entry(hass):
|
||||
async def test_setup_with_config_entry(hass, caplog):
|
||||
"""Test setup component with config."""
|
||||
|
||||
mock_plex_server = MockPlexServer()
|
||||
@@ -109,30 +109,28 @@ async def test_setup_with_config_entry(hass):
|
||||
hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] == const.PLATFORMS
|
||||
)
|
||||
|
||||
async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
await trigger_plex_update(hass, server_id)
|
||||
|
||||
sensor = hass.states.get("sensor.plex_plex_server_1")
|
||||
assert sensor.state == str(len(mock_plex_server.accounts))
|
||||
|
||||
async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
await trigger_plex_update(hass, server_id)
|
||||
|
||||
with patch.object(
|
||||
mock_plex_server, "clients", side_effect=plexapi.exceptions.BadRequest
|
||||
for test_exception in (
|
||||
plexapi.exceptions.BadRequest,
|
||||
requests.exceptions.RequestException,
|
||||
):
|
||||
async_dispatcher_send(
|
||||
hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
with patch.object(
|
||||
mock_plex_server, "clients", side_effect=test_exception
|
||||
) as patched_clients_bad_request:
|
||||
await trigger_plex_update(hass, server_id)
|
||||
|
||||
with patch.object(
|
||||
mock_plex_server, "clients", side_effect=requests.exceptions.RequestException
|
||||
):
|
||||
async_dispatcher_send(
|
||||
hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)
|
||||
assert patched_clients_bad_request.called
|
||||
assert (
|
||||
f"Could not connect to Plex server: {mock_plex_server.friendlyName}"
|
||||
in caplog.text
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
caplog.clear()
|
||||
|
||||
|
||||
async def test_set_config_entry_unique_id(hass):
|
||||
@@ -294,8 +292,7 @@ async def test_setup_with_photo_session(hass):
|
||||
|
||||
server_id = mock_plex_server.machineIdentifier
|
||||
|
||||
async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
await trigger_plex_update(hass, server_id)
|
||||
|
||||
media_player = hass.states.get("media_player.plex_product_title")
|
||||
assert media_player.state == "idle"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for Plex server."""
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
|
||||
from asynctest import patch
|
||||
|
||||
@@ -7,16 +8,19 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||
from homeassistant.components.plex.const import (
|
||||
CONF_IGNORE_NEW_SHARED_USERS,
|
||||
CONF_MONITORED_USERS,
|
||||
DEBOUNCE_TIMEOUT,
|
||||
DOMAIN,
|
||||
PLEX_UPDATE_PLATFORMS_SIGNAL,
|
||||
SERVERS,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .common import trigger_plex_update
|
||||
from .const import DEFAULT_DATA, DEFAULT_OPTIONS
|
||||
from .mock_classes import MockPlexServer
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_new_users_available(hass):
|
||||
@@ -44,8 +48,7 @@ async def test_new_users_available(hass):
|
||||
|
||||
server_id = mock_plex_server.machineIdentifier
|
||||
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
await trigger_plex_update(hass, server_id)
|
||||
|
||||
monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users
|
||||
|
||||
@@ -83,8 +86,7 @@ async def test_new_ignored_users_available(hass, caplog):
|
||||
|
||||
server_id = mock_plex_server.machineIdentifier
|
||||
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
await trigger_plex_update(hass, server_id)
|
||||
|
||||
monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users
|
||||
|
||||
@@ -92,7 +94,7 @@ async def test_new_ignored_users_available(hass, caplog):
|
||||
assert len(monitored_users) == 1
|
||||
assert len(ignored_users) == 2
|
||||
for ignored_user in ignored_users:
|
||||
assert f"Ignoring Plex client owned by {ignored_user}" in caplog.text
|
||||
assert f"Ignoring Plex client owned by '{ignored_user}'" in caplog.text
|
||||
|
||||
sensor = hass.states.get("sensor.plex_plex_server_1")
|
||||
assert sensor.state == str(len(mock_plex_server.accounts))
|
||||
@@ -118,8 +120,7 @@ async def test_mark_sessions_idle(hass):
|
||||
|
||||
server_id = mock_plex_server.machineIdentifier
|
||||
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
await trigger_plex_update(hass, server_id)
|
||||
|
||||
sensor = hass.states.get("sensor.plex_plex_server_1")
|
||||
assert sensor.state == str(len(mock_plex_server.accounts))
|
||||
@@ -127,8 +128,44 @@ async def test_mark_sessions_idle(hass):
|
||||
mock_plex_server.clear_clients()
|
||||
mock_plex_server.clear_sessions()
|
||||
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
await trigger_plex_update(hass, server_id)
|
||||
|
||||
sensor = hass.states.get("sensor.plex_plex_server_1")
|
||||
assert sensor.state == "0"
|
||||
|
||||
|
||||
async def test_debouncer(hass, caplog):
|
||||
"""Test debouncer decorator logic."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=DEFAULT_DATA,
|
||||
options=DEFAULT_OPTIONS,
|
||||
unique_id=DEFAULT_DATA["server_id"],
|
||||
)
|
||||
|
||||
mock_plex_server = MockPlexServer(config_entry=entry)
|
||||
|
||||
with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch(
|
||||
"homeassistant.components.plex.PlexWebsocket.listen"
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
server_id = mock_plex_server.machineIdentifier
|
||||
|
||||
# First two updates are skipped
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id))
|
||||
await hass.async_block_till_done()
|
||||
|
||||
next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT)
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
caplog.text.count(f"Throttling update of {mock_plex_server.friendlyName}") == 2
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@ async def test_show_set_form(hass):
|
||||
async def test_connection_error(hass, aioclient_mock):
|
||||
"""Test we show user form on Twente Milieu connection error."""
|
||||
aioclient_mock.post(
|
||||
"https://wasteapi.2go-mobile.com/api/FetchAdress", exc=aiohttp.ClientError
|
||||
"https://twentemilieuapi.ximmio.com/api/FetchAdress", exc=aiohttp.ClientError
|
||||
)
|
||||
|
||||
flow = config_flow.TwenteMilieuFlowHandler()
|
||||
@@ -49,7 +49,7 @@ async def test_connection_error(hass, aioclient_mock):
|
||||
async def test_invalid_address(hass, aioclient_mock):
|
||||
"""Test we show user form on Twente Milieu invalid address error."""
|
||||
aioclient_mock.post(
|
||||
"https://wasteapi.2go-mobile.com/api/FetchAdress",
|
||||
"https://twentemilieuapi.ximmio.com/api/FetchAdress",
|
||||
json={"dataList": []},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
@@ -70,7 +70,7 @@ async def test_address_already_set_up(hass, aioclient_mock):
|
||||
)
|
||||
|
||||
aioclient_mock.post(
|
||||
"https://wasteapi.2go-mobile.com/api/FetchAdress",
|
||||
"https://twentemilieuapi.ximmio.com/api/FetchAdress",
|
||||
json={"dataList": [{"UniqueId": "12345"}]},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
@@ -86,7 +86,7 @@ async def test_address_already_set_up(hass, aioclient_mock):
|
||||
async def test_full_flow_implementation(hass, aioclient_mock):
|
||||
"""Test registering an integration and finishing flow works."""
|
||||
aioclient_mock.post(
|
||||
"https://wasteapi.2go-mobile.com/api/FetchAdress",
|
||||
"https://twentemilieuapi.ximmio.com/api/FetchAdress",
|
||||
json={"dataList": [{"UniqueId": "12345"}]},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
@@ -18,31 +18,31 @@ async def init_integration(
|
||||
|
||||
fixture = "wled/rgb.json" if not rgbw else "wled/rgbw.json"
|
||||
aioclient_mock.get(
|
||||
"http://example.local:80/json/",
|
||||
"http://192.168.1.123:80/json/",
|
||||
text=load_fixture(fixture),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
aioclient_mock.post(
|
||||
"http://example.local:80/json/state",
|
||||
"http://192.168.1.123:80/json/state",
|
||||
json={},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
aioclient_mock.get(
|
||||
"http://example.local:80/json/info",
|
||||
"http://192.168.1.123:80/json/info",
|
||||
json={},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
aioclient_mock.get(
|
||||
"http://example.local:80/json/state",
|
||||
"http://192.168.1.123:80/json/state",
|
||||
json={},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "example.local", CONF_MAC: "aabbccddeeff"}
|
||||
domain=DOMAIN, data={CONF_HOST: "192.168.1.123", CONF_MAC: "aabbccddeeff"}
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
@@ -40,7 +40,7 @@ async def test_show_zerconf_form(
|
||||
) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
aioclient_mock.get(
|
||||
"http://example.local:80/json/",
|
||||
"http://192.168.1.123:80/json/",
|
||||
text=load_fixture("wled/rgb.json"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
@@ -49,10 +49,10 @@ async def test_show_zerconf_form(
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
result = await flow.async_step_zeroconf(
|
||||
{"hostname": "example.local.", "properties": {}}
|
||||
{"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}
|
||||
)
|
||||
|
||||
assert flow.context[CONF_HOST] == "example.local"
|
||||
assert flow.context[CONF_HOST] == "192.168.1.123"
|
||||
assert flow.context[CONF_NAME] == "example"
|
||||
assert result["description_placeholders"] == {CONF_NAME: "example"}
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
@@ -80,12 +80,12 @@ async def test_zeroconf_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on WLED connection error."""
|
||||
aioclient_mock.get("http://example.local/json/", exc=aiohttp.ClientError)
|
||||
aioclient_mock.get("http://192.168.1.123/json/", exc=aiohttp.ClientError)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data={"hostname": "example.local.", "properties": {}},
|
||||
data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}},
|
||||
)
|
||||
|
||||
assert result["reason"] == "connection_error"
|
||||
@@ -96,7 +96,7 @@ async def test_zeroconf_confirm_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on WLED connection error."""
|
||||
aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError)
|
||||
aioclient_mock.get("http://192.168.1.123:80/json/", exc=aiohttp.ClientError)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
@@ -105,7 +105,7 @@ async def test_zeroconf_confirm_connection_error(
|
||||
CONF_HOST: "example.com",
|
||||
CONF_NAME: "test",
|
||||
},
|
||||
data={"hostname": "example.com.", "properties": {}},
|
||||
data={"host": "192.168.1.123", "hostname": "example.com.", "properties": {}},
|
||||
)
|
||||
|
||||
assert result["reason"] == "connection_error"
|
||||
@@ -133,7 +133,7 @@ async def test_user_device_exists_abort(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_HOST: "example.local"},
|
||||
data={CONF_HOST: "192.168.1.123"},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
@@ -149,7 +149,7 @@ async def test_zeroconf_device_exists_abort(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data={"hostname": "example.local.", "properties": {}},
|
||||
data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
@@ -165,7 +165,11 @@ async def test_zeroconf_with_mac_device_exists_abort(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
config_flow.DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data={"hostname": "example.local.", "properties": {CONF_MAC: "aabbccddeeff"}},
|
||||
data={
|
||||
"host": "192.168.1.123",
|
||||
"hostname": "example.local.",
|
||||
"properties": {CONF_MAC: "aabbccddeeff"},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
@@ -177,7 +181,7 @@ async def test_full_user_flow_implementation(
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.get(
|
||||
"http://example.local:80/json/",
|
||||
"http://192.168.1.123:80/json/",
|
||||
text=load_fixture("wled/rgb.json"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
@@ -190,12 +194,12 @@ async def test_full_user_flow_implementation(
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_HOST: "example.local"}
|
||||
result["flow_id"], user_input={CONF_HOST: "192.168.1.123"}
|
||||
)
|
||||
|
||||
assert result["data"][CONF_HOST] == "example.local"
|
||||
assert result["data"][CONF_HOST] == "192.168.1.123"
|
||||
assert result["data"][CONF_MAC] == "aabbccddeeff"
|
||||
assert result["title"] == "example.local"
|
||||
assert result["title"] == "192.168.1.123"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
|
||||
@@ -204,7 +208,7 @@ async def test_full_zeroconf_flow_implementation(
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.get(
|
||||
"http://example.local:80/json/",
|
||||
"http://192.168.1.123:80/json/",
|
||||
text=load_fixture("wled/rgb.json"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
@@ -213,19 +217,17 @@ async def test_full_zeroconf_flow_implementation(
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
result = await flow.async_step_zeroconf(
|
||||
{"hostname": "example.local.", "properties": {}}
|
||||
{"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}
|
||||
)
|
||||
|
||||
assert flow.context[CONF_HOST] == "example.local"
|
||||
assert flow.context[CONF_HOST] == "192.168.1.123"
|
||||
assert flow.context[CONF_NAME] == "example"
|
||||
assert result["description_placeholders"] == {CONF_NAME: "example"}
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await flow.async_step_zeroconf_confirm(
|
||||
user_input={CONF_HOST: "example.local"}
|
||||
)
|
||||
assert result["data"][CONF_HOST] == "example.local"
|
||||
result = await flow.async_step_zeroconf_confirm(user_input={})
|
||||
assert result["data"][CONF_HOST] == "192.168.1.123"
|
||||
assert result["data"][CONF_MAC] == "aabbccddeeff"
|
||||
assert result["title"] == "example"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
@@ -13,7 +13,7 @@ async def test_config_entry_not_ready(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the WLED configuration entry not ready."""
|
||||
aioclient_mock.get("http://example.local:80/json/", exc=aiohttp.ClientError)
|
||||
aioclient_mock.get("http://192.168.1.123:80/json/", exc=aiohttp.ClientError)
|
||||
|
||||
entry = await init_integration(hass, aioclient_mock)
|
||||
assert entry.state == ENTRY_STATE_SETUP_RETRY
|
||||
|
||||
@@ -141,7 +141,7 @@ async def test_light_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
|
||||
) -> None:
|
||||
"""Test error handling of the WLED lights."""
|
||||
aioclient_mock.post("http://example.local:80/json/state", text="", status=400)
|
||||
aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400)
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"):
|
||||
@@ -162,7 +162,7 @@ async def test_light_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test error handling of the WLED switches."""
|
||||
aioclient_mock.post("http://example.local:80/json/state", exc=aiohttp.ClientError)
|
||||
aioclient_mock.post("http://192.168.1.123:80/json/state", exc=aiohttp.ClientError)
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"):
|
||||
@@ -339,7 +339,7 @@ async def test_effect_service_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
|
||||
) -> None:
|
||||
"""Test error handling of the WLED effect service."""
|
||||
aioclient_mock.post("http://example.local:80/json/state", text="", status=400)
|
||||
aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400)
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"):
|
||||
|
||||
@@ -139,7 +139,7 @@ async def test_switch_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog
|
||||
) -> None:
|
||||
"""Test error handling of the WLED switches."""
|
||||
aioclient_mock.post("http://example.local:80/json/state", text="", status=400)
|
||||
aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400)
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"):
|
||||
@@ -160,7 +160,7 @@ async def test_switch_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test error handling of the WLED switches."""
|
||||
aioclient_mock.post("http://example.local:80/json/state", exc=aiohttp.ClientError)
|
||||
aioclient_mock.post("http://192.168.1.123:80/json/state", exc=aiohttp.ClientError)
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"):
|
||||
|
||||
Reference in New Issue
Block a user