Compare commits

...

25 Commits

Author SHA1 Message Date
Paulus Schoutsen
0f39296251 Bumped version to 0.108.0b3 2020-04-04 23:54:06 -07:00
J. Nick Koston
dd0fd36049 Handle float values for homekit lightning (#33683)
* Handle float values for homekit lightning

* Empty commit to rerun CI
2020-04-04 23:53:58 -07:00
jjlawren
30a391b88b Plex logging additions & cleanup (#33681) 2020-04-04 23:53:57 -07:00
Alexei Chetroi
71803cbdef Update zha dependencies (#33639) 2020-04-04 23:53:56 -07:00
Franck Nijhof
f5eafbe760 Bump twentemilieu to 0.3.0 (#33622)
* Bump twentemilieu to 0.3.0

* Fix tests
2020-04-04 23:53:12 -07:00
J. Nick Koston
52f710528f Handle race condition in harmony setup (#33611)
* Handle race condition in harmony setup

If the remote was discovered via ssdp before the yaml config import
happened, the unique id would already be set and the import
would abort.

* Update homeassistant/components/harmony/config_flow.py

Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>

* reduce

* black

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-04-04 23:52:27 -07:00
Chris Talkington
38b729b00a Use IP addresses instead of mDNS names when IPP discovered (#33610)
* use discovery resolved host rather than mdns host.

* Update __init__.py

* Update test_config_flow.py

* Update __init__.py

* Update test_init.py

* Update test_config_flow.py

* Update test_config_flow.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update test_init.py

* Update test_config_flow.py
2020-04-04 23:52:26 -07:00
Anders Melchiorsen
5dae7f8451 Identify more Sonos radio stations with poor titles (#33609) 2020-04-04 23:52:25 -07:00
Paulus Schoutsen
6a297b3758 Use IP addresses instead of mDNS names when wled discovered (#33608) 2020-04-04 23:52:25 -07:00
Paulus Schoutsen
ab7afbdaf7 Hass.io integration do not warn safe mode (#33600)
* Hass.io integration do not warn safe mode

* Better implementation

* Tweak log message
2020-04-04 23:52:24 -07:00
jjlawren
0763503151 Debounce calls to Plex server (#33560)
* Debounce calls to Plex server

* Simplify debounce by recommendation

* Update tests to handle debounce

* Test debouncer, fix & optimize tests

* Use property instead
2020-04-04 23:52:23 -07:00
Paulus Schoutsen
4ead87270e Bumped version to 0.108.0b2 2020-04-03 10:48:05 -07:00
Bram Kragten
1634592d90 Updated frontend to 20200403.0 (#33586) 2020-04-03 10:46:55 -07:00
Bram Kragten
ddddd8566d Add default delay to Harmony config entries (#33576) 2020-04-03 10:46:24 -07:00
Franck Nijhof
5cf2043c04 Bump adguardhome to 0.4.2 (#33575) 2020-04-03 10:46:23 -07:00
Jc2k
43777ace20 Fix browsing regression (#33572) 2020-04-03 10:46:22 -07:00
Maciej Bieniek
a7e5cc31c3 Bump gios library to version 0.1.1 (#33569) 2020-04-03 10:46:22 -07:00
Maciej Bieniek
aa6520cac1 Fix source name (#33565) 2020-04-03 10:46:21 -07:00
Fabian Affolter
af10cd315e Upgrade luftdaten to 0.6.4 (#33564) 2020-04-03 10:46:20 -07:00
Eugenio Panadero
ef28bcaa9c Identify cameras in error logs for generic and mjpeg cameras (#33561) 2020-04-03 10:46:19 -07:00
jjlawren
ff3bfade31 Plex followup to #33542 (#33558) 2020-04-03 10:46:18 -07:00
J. Nick Koston
4eafd8adf7 Add missing flow_title to doorbird (#33557)
When placeholders are in use, flow_title needs to be
set in the json to prevent an empty name in the
integrations dashboard. This affected doorbirds
that were found via ssdp.
2020-04-03 10:46:17 -07:00
Paulus Schoutsen
cb5de0e090 Convert TTS tests to async (#33517)
* Convert TTS tests to async

* Address comments
2020-04-03 10:46:17 -07:00
ollo69
254394ecab Fix asuswrt network failure startup (#33485)
* Fix network failure startup

Fix for issue ##33284 - Asuswrt component fail at startup after power failure

* Removed comment

* Removed bare except

* is_connected moved out try-catch

* Removed pointless-string-statement

* Raise PlatformNotReady on "not is_connected"

* Removed unnecessary check

* Revert "Removed unnecessary check"

This reverts commit a2ccddab2c4b1ba441f1d7482d802d9774527a26.

* Implemented custom retry mechanism

* Fix new line missing

* Fix formatting

* Fix indent

* Reviewed check

* Recoded based on tibber implementation

* Formatting review

* Changes requested

* Fix tests for setup retry

* Updated missing test

* Fixed check on Tests

* Return false if not exception

* Format correction
2020-04-03 10:46:16 -07:00
J. Nick Koston
e4e0c37a8c Use homekit service callbacks for lights to resolve out of sync states (#32348)
* Switch homekit lights to use service callbacks

Service callbacks allow us to get the on/off, brightness, etc
all in one call so we remove all the complexity that was
previously needed to handle the out of sync states

We now get the on event and brightness event at the same time
which allows us to prevent lights from flashing up to 100%
before the requested brightness.

* Fix STATE_OFF -> STATE_ON,brightness:0
2020-04-03 10:46:15 -07:00
47 changed files with 1186 additions and 853 deletions

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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

View File

@@ -21,7 +21,8 @@
"title": "Connect to the DoorBird"
}
},
"title": "DoorBird"
"title": "DoorBird",
"flow_title" : "DoorBird {name} ({host})"
},
"options": {
"step": {
@@ -33,4 +34,4 @@
}
}
}
}
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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")

View File

@@ -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."""

View File

@@ -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
}

View File

@@ -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:

View File

@@ -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):

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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"

View File

@@ -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."""

View File

@@ -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"

View File

@@ -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":

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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."""

View 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"]
}

View File

@@ -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},

View File

@@ -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"

View File

@@ -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"
],

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 = []

View File

@@ -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:

View File

@@ -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"
)

View File

@@ -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,

View File

@@ -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]

View File

@@ -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

View 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()

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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"},
)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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"):

View File

@@ -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"):