Merge pull request #35092 from home-assistant/rc

This commit is contained in:
Paulus Schoutsen 2020-05-02 16:30:19 -07:00 committed by GitHub
commit 5f267be1db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 290 additions and 125 deletions

View File

@ -2,7 +2,7 @@
"domain": "braviatv",
"name": "Sony Bravia TV",
"documentation": "https://www.home-assistant.io/integrations/braviatv",
"requirements": ["bravia-tv==1.0.2"],
"requirements": ["bravia-tv==1.0.3"],
"codeowners": ["@robbiet480", "@bieniu"],
"config_flow": true
}

View File

@ -81,7 +81,7 @@ class CanaryCamera(Camera):
async def async_camera_image(self):
"""Return a still image response from the camera."""
self.renew_live_stream_session()
await self.hass.async_add_executor_job(self.renew_live_stream_session)
ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
image = await asyncio.shield(

View File

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

View File

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

View File

@ -275,13 +275,12 @@ def get_accessory(hass, driver, state, aid, config):
elif state.domain == "media_player":
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
feature_list = config.get(CONF_FEATURE_LIST)
feature_list = config.get(CONF_FEATURE_LIST, [])
if device_class == DEVICE_CLASS_TV:
a_type = "TelevisionMediaPlayer"
else:
if feature_list and validate_media_player_features(state, feature_list):
a_type = "MediaPlayer"
elif validate_media_player_features(state, feature_list):
a_type = "MediaPlayer"
elif state.domain == "sensor":
device_class = state.attributes.get(ATTR_DEVICE_CLASS)

View File

@ -65,6 +65,7 @@ from .const import (
SERV_TELEVISION,
SERV_TELEVISION_SPEAKER,
)
from .util import get_media_player_features
_LOGGER = logging.getLogger(__name__)
@ -84,10 +85,12 @@ MEDIA_PLAYER_KEYS = {
# 15: "Information",
}
# Names may not contain special characters
# or emjoi (/ is a special character for Apple)
MODE_FRIENDLY_NAME = {
FEATURE_ON_OFF: "Power",
FEATURE_PLAY_PAUSE: "Play/Pause",
FEATURE_PLAY_STOP: "Play/Stop",
FEATURE_PLAY_PAUSE: "Play-Pause",
FEATURE_PLAY_STOP: "Play-Stop",
FEATURE_TOGGLE_MUTE: "Mute",
}
@ -106,7 +109,9 @@ class MediaPlayer(HomeAccessory):
FEATURE_PLAY_STOP: None,
FEATURE_TOGGLE_MUTE: None,
}
feature_list = self.config[CONF_FEATURE_LIST]
feature_list = self.config.get(
CONF_FEATURE_LIST, get_media_player_features(state)
)
if FEATURE_ON_OFF in feature_list:
name = self.generate_service_name(FEATURE_ON_OFF)
@ -214,7 +219,7 @@ class MediaPlayer(HomeAccessory):
self.chars[FEATURE_PLAY_STOP].set_value(hk_state)
if self.chars[FEATURE_TOGGLE_MUTE]:
current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)
current_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED))
_LOGGER.debug(
'%s: Set current state for "toggle_mute" to %s',
self.entity_id,
@ -240,9 +245,7 @@ class TelevisionMediaPlayer(HomeAccessory):
# Add additional characteristics if volume or input selection supported
self.chars_tv = []
self.chars_speaker = []
features = self.hass.states.get(self.entity_id).attributes.get(
ATTR_SUPPORTED_FEATURES, 0
)
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if features & (SUPPORT_PLAY | SUPPORT_PAUSE):
self.chars_tv.append(CHAR_REMOTE_KEY)
@ -253,7 +256,8 @@ class TelevisionMediaPlayer(HomeAccessory):
if features & SUPPORT_VOLUME_SET:
self.chars_speaker.append(CHAR_VOLUME)
if features & SUPPORT_SELECT_SOURCE:
source_list = state.attributes.get(ATTR_INPUT_SOURCE_LIST, [])
if source_list and features & SUPPORT_SELECT_SOURCE:
self.support_select_source = True
serv_tv = self.add_preload_service(SERV_TELEVISION, self.chars_tv)
@ -298,9 +302,7 @@ class TelevisionMediaPlayer(HomeAccessory):
)
if self.support_select_source:
self.sources = self.hass.states.get(self.entity_id).attributes.get(
ATTR_INPUT_SOURCE_LIST, []
)
self.sources = source_list
self.char_input_source = serv_tv.configure_char(
CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_source
)
@ -380,14 +382,13 @@ class TelevisionMediaPlayer(HomeAccessory):
hk_state = 0
if current_state not in ("None", STATE_OFF, STATE_UNKNOWN):
hk_state = 1
_LOGGER.debug("%s: Set current active state to %s", self.entity_id, hk_state)
if self.char_active.value != hk_state:
self.char_active.set_value(hk_state)
# Set mute state
if CHAR_VOLUME_SELECTOR in self.chars_speaker:
current_mute_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)
current_mute_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED))
_LOGGER.debug(
"%s: Set current mute state to %s", self.entity_id, current_mute_state,
)
@ -395,20 +396,16 @@ class TelevisionMediaPlayer(HomeAccessory):
self.char_mute.set_value(current_mute_state)
# Set active input
if self.support_select_source:
if self.support_select_source and self.sources:
source_name = new_state.attributes.get(ATTR_INPUT_SOURCE)
if self.sources:
_LOGGER.debug(
"%s: Set current input to %s", self.entity_id, source_name
_LOGGER.debug("%s: Set current input to %s", self.entity_id, source_name)
if source_name in self.sources:
index = self.sources.index(source_name)
if self.char_input_source.value != index:
self.char_input_source.set_value(index)
else:
_LOGGER.warning(
"%s: Sources out of sync. Restart Home Assistant", self.entity_id,
)
if source_name in self.sources:
index = self.sources.index(source_name)
if self.char_input_source.value != index:
self.char_input_source.set_value(index)
else:
_LOGGER.warning(
"%s: Sources out of sync. Restart Home Assistant",
self.entity_id,
)
if self.char_input_source.value != 0:
self.char_input_source.set_value(0)
if self.char_input_source.value != 0:
self.char_input_source.set_value(0)

View File

@ -96,6 +96,40 @@ SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend(
)
HOMEKIT_CHAR_TRANSLATIONS = {
0: " ", # nul
10: " ", # nl
13: " ", # cr
33: "-", # !
34: " ", # "
36: "-", # $
37: "-", # %
40: "-", # (
41: "-", # )
42: "-", # *
43: "-", # +
47: "-", # /
58: "-", # :
59: "-", # ;
60: "-", # <
61: "-", # =
62: "-", # >
63: "-", # ?
64: "-", # @
91: "-", # [
92: "-", # \
93: "-", # ]
94: "-", # ^
95: " ", # _
96: "-", # `
123: "-", # {
124: "-", # |
125: "-", # }
126: "-", # ~
127: "-", # del
}
def validate_entity_config(values):
"""Validate config entry for CONF_ENTITY."""
if not isinstance(values, dict):
@ -133,8 +167,8 @@ def validate_entity_config(values):
return entities
def validate_media_player_features(state, feature_list):
"""Validate features for media players."""
def get_media_player_features(state):
"""Determine features for media players."""
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
supported_modes = []
@ -148,6 +182,20 @@ def validate_media_player_features(state, feature_list):
supported_modes.append(FEATURE_PLAY_STOP)
if features & media_player.const.SUPPORT_VOLUME_MUTE:
supported_modes.append(FEATURE_TOGGLE_MUTE)
return supported_modes
def validate_media_player_features(state, feature_list):
"""Validate features for media players."""
supported_modes = get_media_player_features(state)
if not supported_modes:
_LOGGER.error("%s does not support any media_player features", state.entity_id)
return False
if not feature_list:
# Auto detected
return True
error_list = []
for feature in feature_list:
@ -155,7 +203,9 @@ def validate_media_player_features(state, feature_list):
error_list.append(feature)
if error_list:
_LOGGER.error("%s does not support features: %s", state.entity_id, error_list)
_LOGGER.error(
"%s does not support media_player features: %s", state.entity_id, error_list
)
return False
return True
@ -247,6 +297,16 @@ def convert_to_float(state):
return None
def cleanup_name_for_homekit(name):
"""Ensure the name of the device will not crash homekit."""
#
# This is not a security measure.
#
# UNICODE_EMOJI is also not allowed but that
# likely isn't a problem
return name.translate(HOMEKIT_CHAR_TRANSLATIONS)
def temperature_to_homekit(temperature, unit):
"""Convert temperature to Celsius for HomeKit."""
return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1)

View File

@ -31,7 +31,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.temperature import convert as convert_temperature
from . import MelCloudDevice
from .const import (
@ -44,7 +43,6 @@ from .const import (
DOMAIN,
SERVICE_SET_VANE_HORIZONTAL,
SERVICE_SET_VANE_VERTICAL,
TEMP_UNIT_LOOKUP,
)
SCAN_INTERVAL = timedelta(seconds=60)
@ -169,7 +167,7 @@ class AtaDeviceClimate(MelCloudClimate):
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement used by the platform."""
return TEMP_UNIT_LOOKUP.get(self._device.temp_unit, TEMP_CELSIUS)
return TEMP_CELSIUS
@property
def hvac_mode(self) -> str:
@ -281,9 +279,7 @@ class AtaDeviceClimate(MelCloudClimate):
if min_value is not None:
return min_value
return convert_temperature(
DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit
)
return DEFAULT_MIN_TEMP
@property
def max_temp(self) -> float:
@ -292,9 +288,7 @@ class AtaDeviceClimate(MelCloudClimate):
if max_value is not None:
return max_value
return convert_temperature(
DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit
)
return DEFAULT_MAX_TEMP
class AtwDeviceZoneClimate(MelCloudClimate):
@ -331,7 +325,7 @@ class AtwDeviceZoneClimate(MelCloudClimate):
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement used by the platform."""
return TEMP_UNIT_LOOKUP.get(self._device.temp_unit, TEMP_CELSIUS)
return TEMP_CELSIUS
@property
def hvac_mode(self) -> str:
@ -391,7 +385,7 @@ class AtwDeviceZoneClimate(MelCloudClimate):
MELCloud API does not expose radiator zone temperature limits.
"""
return convert_temperature(10, TEMP_CELSIUS, self.temperature_unit)
return 10
@property
def max_temp(self) -> float:
@ -399,4 +393,4 @@ class AtwDeviceZoneClimate(MelCloudClimate):
MELCloud API does not expose radiator zone temperature limits.
"""
return convert_temperature(30, TEMP_CELSIUS, self.temperature_unit)
return 30

View File

@ -1,7 +1,4 @@
"""Constants for the MELCloud Climate integration."""
from pymelcloud.const import UNIT_TEMP_CELSIUS, UNIT_TEMP_FAHRENHEIT
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
DOMAIN = "melcloud"
@ -15,9 +12,3 @@ ATTR_VANE_VERTICAL_POSITIONS = "vane_vertical_positions"
SERVICE_SET_VANE_HORIZONTAL = "set_vane_horizontal"
SERVICE_SET_VANE_VERTICAL = "set_vane_vertical"
TEMP_UNIT_LOOKUP = {
UNIT_TEMP_CELSIUS: TEMP_CELSIUS,
UNIT_TEMP_FAHRENHEIT: TEMP_FAHRENHEIT,
}
TEMP_UNIT_REVERSE_LOOKUP = {v: k for k, v in TEMP_UNIT_LOOKUP.items()}

View File

@ -12,11 +12,11 @@ from homeassistant.const import (
from homeassistant.helpers.entity import Entity
from . import MelCloudDevice
from .const import DOMAIN, TEMP_UNIT_LOOKUP
from .const import DOMAIN
ATTR_MEASUREMENT_NAME = "measurement_name"
ATTR_ICON = "icon"
ATTR_UNIT_FN = "unit_fn"
ATTR_UNIT = "unit"
ATTR_DEVICE_CLASS = "device_class"
ATTR_VALUE_FN = "value_fn"
ATTR_ENABLED_FN = "enabled"
@ -25,7 +25,7 @@ ATA_SENSORS = {
"room_temperature": {
ATTR_MEASUREMENT_NAME: "Room Temperature",
ATTR_ICON: "mdi:thermometer",
ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS),
ATTR_UNIT: TEMP_CELSIUS,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_VALUE_FN: lambda x: x.device.room_temperature,
ATTR_ENABLED_FN: lambda x: True,
@ -33,7 +33,7 @@ ATA_SENSORS = {
"energy": {
ATTR_MEASUREMENT_NAME: "Energy",
ATTR_ICON: "mdi:factory",
ATTR_UNIT_FN: lambda x: ENERGY_KILO_WATT_HOUR,
ATTR_UNIT: ENERGY_KILO_WATT_HOUR,
ATTR_DEVICE_CLASS: None,
ATTR_VALUE_FN: lambda x: x.device.total_energy_consumed,
ATTR_ENABLED_FN: lambda x: x.device.has_energy_consumed_meter,
@ -43,7 +43,7 @@ ATW_SENSORS = {
"outside_temperature": {
ATTR_MEASUREMENT_NAME: "Outside Temperature",
ATTR_ICON: "mdi:thermometer",
ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS),
ATTR_UNIT: TEMP_CELSIUS,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_VALUE_FN: lambda x: x.device.outside_temperature,
ATTR_ENABLED_FN: lambda x: True,
@ -51,7 +51,7 @@ ATW_SENSORS = {
"tank_temperature": {
ATTR_MEASUREMENT_NAME: "Tank Temperature",
ATTR_ICON: "mdi:thermometer",
ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS),
ATTR_UNIT: TEMP_CELSIUS,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_VALUE_FN: lambda x: x.device.tank_temperature,
ATTR_ENABLED_FN: lambda x: True,
@ -61,7 +61,7 @@ ATW_ZONE_SENSORS = {
"room_temperature": {
ATTR_MEASUREMENT_NAME: "Room Temperature",
ATTR_ICON: "mdi:thermometer",
ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS),
ATTR_UNIT: TEMP_CELSIUS,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_VALUE_FN: lambda zone: zone.room_temperature,
ATTR_ENABLED_FN: lambda x: True,
@ -131,7 +131,7 @@ class MelDeviceSensor(Entity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._def[ATTR_UNIT_FN](self._api)
return self._def[ATTR_UNIT]
@property
def device_class(self):

View File

@ -18,7 +18,7 @@ from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN, MelCloudDevice
from .const import ATTR_STATUS, TEMP_UNIT_LOOKUP
from .const import ATTR_STATUS
async def async_setup_entry(
@ -80,7 +80,7 @@ class AtwWaterHeater(WaterHeaterDevice):
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement used by the platform."""
return TEMP_UNIT_LOOKUP.get(self._device.temp_unit, TEMP_CELSIUS)
return TEMP_CELSIUS
@property
def current_operation(self) -> Optional[str]:

View File

@ -3,7 +3,7 @@
"name": "iRobot Roomba",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/roomba",
"requirements": ["roombapy==1.5.1"],
"requirements": ["roombapy==1.5.2"],
"dependencies": [],
"codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"]
}

View File

@ -2,6 +2,7 @@
from homeassistant.const import (
DATA_MEGABYTES,
DATA_RATE_KILOBYTES_PER_SECOND,
DATA_TERABYTES,
UNIT_PERCENTAGE,
)
@ -34,8 +35,8 @@ UTILISATION_SENSORS = {
STORAGE_VOL_SENSORS = {
"volume_status": ["Status", None, "mdi:checkbox-marked-circle-outline"],
"volume_device_type": ["Type", None, "mdi:harddisk"],
"volume_size_total": ["Total Size", None, "mdi:chart-pie"],
"volume_size_used": ["Used Space", None, "mdi:chart-pie"],
"volume_size_total": ["Total Size", DATA_TERABYTES, "mdi:chart-pie"],
"volume_size_used": ["Used Space", DATA_TERABYTES, "mdi:chart-pie"],
"volume_percentage_used": ["Volume Used", UNIT_PERCENTAGE, "mdi:chart-pie"],
"volume_disk_temp_avg": ["Average Disk Temp", None, "mdi:thermometer"],
"volume_disk_temp_max": ["Maximum Disk Temp", None, "mdi:thermometer"],

View File

@ -2,7 +2,7 @@
"domain": "synology_dsm",
"name": "Synology DSM",
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"requirements": ["python-synology==0.7.3"],
"requirements": ["python-synology==0.8.0"],
"codeowners": ["@ProtoThis", "@Quentame"],
"config_flow": true,
"ssdp": [

View File

@ -7,11 +7,13 @@ from homeassistant.const import (
CONF_DISKS,
DATA_MEGABYTES,
DATA_RATE_KILOBYTES_PER_SECOND,
DATA_TERABYTES,
TEMP_CELSIUS,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.temperature import celsius_to_fahrenheit
from . import SynoApi
from .const import (
@ -63,7 +65,7 @@ async def async_setup_entry(
class SynoNasSensor(Entity):
"""Representation of a Synology NAS Sensor."""
"""Representation of a Synology NAS sensor."""
def __init__(
self,
@ -142,47 +144,51 @@ class SynoNasSensor(Entity):
class SynoNasUtilSensor(SynoNasSensor):
"""Representation a Synology Utilisation Sensor."""
"""Representation a Synology Utilisation sensor."""
@property
def state(self):
"""Return the state."""
if self._unit == DATA_RATE_KILOBYTES_PER_SECOND or self._unit == DATA_MEGABYTES:
attr = getattr(self._api.utilisation, self.sensor_type)(False)
attr = getattr(self._api.utilisation, self.sensor_type)
if callable(attr):
attr = attr()
if not attr:
return None
if attr is None:
return None
# Data (RAM)
if self._unit == DATA_MEGABYTES:
return round(attr / 1024.0 ** 2, 1)
if self._unit == DATA_RATE_KILOBYTES_PER_SECOND:
return round(attr / 1024.0, 1)
if self._unit == DATA_MEGABYTES:
return round(attr / 1024.0 / 1024.0, 1)
else:
return getattr(self._api.utilisation, self.sensor_type)
# Network
if self._unit == DATA_RATE_KILOBYTES_PER_SECOND:
return round(attr / 1024.0, 1)
return attr
class SynoNasStorageSensor(SynoNasSensor):
"""Representation a Synology Storage Sensor."""
"""Representation a Synology Storage sensor."""
@property
def state(self):
"""Return the state."""
if self.monitored_device:
if self.sensor_type in TEMP_SENSORS_KEYS:
attr = getattr(self._api.storage, self.sensor_type)(
self.monitored_device
)
attr = getattr(self._api.storage, self.sensor_type)(self.monitored_device)
if not attr:
return None
if attr is None:
return None
# Data (disk space)
if self._unit == DATA_TERABYTES:
return round(attr / 1024.0 ** 4, 2)
if self._api.temp_unit == TEMP_CELSIUS:
return attr
# Temperature
if self._api.temp_unit == TEMP_CELSIUS:
# Celsius
return attr
if self.sensor_type in TEMP_SENSORS_KEYS:
# Fahrenheit
return celsius_to_fahrenheit(attr)
return round(attr * 1.8 + 32.0, 1)
return getattr(self._api.storage, self.sensor_type)(self.monitored_device)
return None
return attr
@property
def device_info(self) -> Dict[str, any]:

View File

@ -32,7 +32,12 @@ from .const import (
LOGGER,
)
from .controller import get_controller
from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect
from .errors import (
AlreadyConfigured,
AuthenticationRequired,
CannotConnect,
NoLocalUser,
)
DEFAULT_PORT = 8443
DEFAULT_SITE_ID = "default"
@ -135,6 +140,8 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
for site in self.sites.values():
if desc == site["desc"]:
if "role" not in site:
raise NoLocalUser
self.config[CONF_SITE_ID] = site["name"]
break
@ -153,6 +160,9 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
except AlreadyConfigured:
return self.async_abort(reason="already_configured")
except NoLocalUser:
return self.async_abort(reason="no_local_user")
if len(self.sites) == 1:
self.desc = next(iter(self.sites.values()))["desc"]
return await self.async_step_site(user_input={})
@ -189,7 +199,12 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
self.options.update(user_input)
return await self.async_step_client_control()
ssid_filter = {wlan: wlan for wlan in self.controller.api.wlans}
ssids = list(self.controller.api.wlans) + [
f"{wlan.name}{wlan.name_combine_suffix}"
for wlan in self.controller.api.wlans.values()
if not wlan.name_combine_enabled
]
ssid_filter = {ssid: ssid for ssid in sorted(ssids)}
return self.async_show_form(
step_id="device_tracker",

View File

@ -188,7 +188,7 @@ class UniFiController:
elif signal == SIGNAL_DATA and data:
if DATA_EVENT in data:
if data[DATA_EVENT].event in (
if next(iter(data[DATA_EVENT])).event in (
WIRELESS_CLIENT_CONNECTED,
WIRELESS_GUEST_CONNECTED,
):

View File

@ -22,5 +22,9 @@ class LoginRequired(UnifiException):
"""Component got logged out."""
class NoLocalUser(UnifiException):
"""No local user."""
class UserLevel(UnifiException):
"""User level too low."""

View File

@ -3,7 +3,7 @@
"name": "Ubiquiti UniFi",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifi",
"requirements": ["aiounifi==18"],
"requirements": ["aiounifi==20"],
"codeowners": ["@kane610"],
"quality_scale": "platinum"
}

View File

@ -20,6 +20,7 @@
},
"abort": {
"already_configured": "Controller site is already configured",
"no_local_user": "No local user found, configure a local account on controller and try again",
"user_privilege": "User needs to be administrator"
}
},

View File

@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "Controller site is already configured",
"no_local_user": "No local user found, configure a local account on controller and try again",
"user_privilege": "User needs to be administrator"
},
"error": {

View File

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

View File

@ -210,6 +210,9 @@ async def async_get_component_strings(
else:
files_to_load[loaded] = path
if not files_to_load:
return translations
# Load files
load_translations_job = hass.async_add_executor_job(
load_translations_files, files_to_load
@ -218,12 +221,12 @@ async def async_get_component_strings(
loaded_translations = await load_translations_job
# Translations that miss "title" will get integration put in.
for loaded, translations in loaded_translations.items():
for loaded, loaded_translation in loaded_translations.items():
if "." in loaded:
continue
if "title" not in translations:
translations["title"] = integrations[loaded].name
if "title" not in loaded_translation:
loaded_translation["title"] = integrations[loaded].name
translations.update(loaded_translations)

View File

@ -11,8 +11,8 @@ ciso8601==2.1.3
cryptography==2.9
defusedxml==0.6.0
distro==1.5.0
hass-nabucasa==0.34.1
home-assistant-frontend==20200427.1
hass-nabucasa==0.34.2
home-assistant-frontend==20200427.2
importlib-metadata==1.6.0
jinja2>=2.11.1
netdisco==2.6.0

View File

@ -212,7 +212,7 @@ aiopylgtv==0.3.3
aioswitcher==1.1.0
# homeassistant.components.unifi
aiounifi==18
aiounifi==20
# homeassistant.components.wwlln
aiowwlln==2.0.2
@ -359,7 +359,7 @@ bomradarloop==0.1.4
boto3==1.9.252
# homeassistant.components.braviatv
bravia-tv==1.0.2
bravia-tv==1.0.3
# homeassistant.components.broadlink
broadlink==0.13.2
@ -683,7 +683,7 @@ habitipy==0.2.0
hangups==0.4.9
# homeassistant.components.cloud
hass-nabucasa==0.34.1
hass-nabucasa==0.34.2
# homeassistant.components.mqtt
hbmqtt==0.9.5
@ -713,7 +713,7 @@ hole==0.5.1
holidays==0.10.2
# homeassistant.components.frontend
home-assistant-frontend==20200427.1
home-assistant-frontend==20200427.2
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -1680,7 +1680,7 @@ python-sochain-api==0.0.2
python-songpal==0.11.2
# homeassistant.components.synology_dsm
python-synology==0.7.3
python-synology==0.8.0
# homeassistant.components.tado
python-tado==0.8.1
@ -1825,7 +1825,7 @@ rocketchat-API==0.6.1
roku==4.1.0
# homeassistant.components.roomba
roombapy==1.5.1
roombapy==1.5.2
# homeassistant.components.rova
rova==0.1.0

View File

@ -95,7 +95,7 @@ aiopylgtv==0.3.3
aioswitcher==1.1.0
# homeassistant.components.unifi
aiounifi==18
aiounifi==20
# homeassistant.components.wwlln
aiowwlln==2.0.2
@ -141,7 +141,7 @@ bellows-homeassistant==0.15.2
bomradarloop==0.1.4
# homeassistant.components.braviatv
bravia-tv==1.0.2
bravia-tv==1.0.3
# homeassistant.components.broadlink
broadlink==0.13.2
@ -273,7 +273,7 @@ ha-ffmpeg==2.0
hangups==0.4.9
# homeassistant.components.cloud
hass-nabucasa==0.34.1
hass-nabucasa==0.34.2
# homeassistant.components.mqtt
hbmqtt==0.9.5
@ -291,7 +291,7 @@ hole==0.5.1
holidays==0.10.2
# homeassistant.components.frontend
home-assistant-frontend==20200427.1
home-assistant-frontend==20200427.2
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@ -650,7 +650,7 @@ python-miio==0.5.0.1
python-nest==4.1.0
# homeassistant.components.synology_dsm
python-synology==0.7.3
python-synology==0.8.0
# homeassistant.components.tado
python-tado==0.8.1
@ -701,7 +701,7 @@ ring_doorbell==0.6.0
roku==4.1.0
# homeassistant.components.roomba
roombapy==1.5.1
roombapy==1.5.2
# homeassistant.components.yamaha
rxv==0.6.0

View File

@ -368,6 +368,26 @@ async def test_media_player_television_basic(hass, hk_driver, events, caplog):
assert not caplog.messages or "Error" not in caplog.messages[-1]
async def test_media_player_television_supports_source_select_no_sources(
hass, hk_driver, events, caplog
):
"""Test if basic tv that supports source select but is missing a source list."""
entity_id = "media_player.television"
# Supports turn_on', 'turn_off'
hass.states.async_set(
entity_id,
None,
{ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, ATTR_SUPPORTED_FEATURES: 3469},
)
await hass.async_block_till_done()
acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None)
await acc.run_handler()
await hass.async_block_till_done()
assert acc.support_select_source is False
async def test_tv_restore(hass, hk_driver, events):
"""Test setting up an entity from state in the event registry."""
hass.state = CoreState.not_running

View File

@ -22,6 +22,7 @@ from homeassistant.components.homekit.const import (
from homeassistant.components.homekit.util import (
HomeKitSpeedMapping,
SpeedRange,
cleanup_name_for_homekit,
convert_to_float,
density_to_air_quality,
dismiss_setup_message,
@ -172,6 +173,19 @@ def test_convert_to_float():
assert convert_to_float(None) is None
def test_cleanup_name_for_homekit():
"""Ensure name sanitize works as expected."""
assert cleanup_name_for_homekit("abc") == "abc"
assert cleanup_name_for_homekit("a b c") == "a b c"
assert cleanup_name_for_homekit("ab_c") == "ab c"
assert (
cleanup_name_for_homekit('ab!@#$%^&*()-=":.,><?//\\ frog')
== "ab--#---&----- -.,------ frog"
)
assert cleanup_name_for_homekit("の日本_語文字セット") == "の日本 語文字セット"
def test_temperature_to_homekit():
"""Test temperature conversion from HA to HomeKit."""
assert temperature_to_homekit(20.46, TEMP_CELSIUS) == 20.5

View File

@ -17,6 +17,7 @@ from homeassistant.components.unifi.const import (
CONF_TRACK_CLIENTS,
CONF_TRACK_DEVICES,
CONF_TRACK_WIRED_CLIENTS,
DOMAIN as UNIFI_DOMAIN,
)
from homeassistant.const import (
CONF_HOST,
@ -32,7 +33,10 @@ from tests.common import MockConfigEntry
CLIENTS = [{"mac": "00:00:00:00:00:01"}]
WLANS = [{"name": "SSID 1"}, {"name": "SSID 2"}]
WLANS = [
{"name": "SSID 1"},
{"name": "SSID 2", "name_combine_enabled": False, "name_combine_suffix": "_IOT"},
]
async def test_flow_works(hass, aioclient_mock, mock_discovery):
@ -183,6 +187,53 @@ async def test_flow_fails_site_already_configured(hass, aioclient_mock):
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_flow_fails_site_has_no_local_user(hass, aioclient_mock):
"""Test config flow."""
entry = MockConfigEntry(
domain=UNIFI_DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}}
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
UNIFI_DOMAIN, context={"source": "user"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
aioclient_mock.get("https://1.2.3.4:1234", status=302)
aioclient_mock.post(
"https://1.2.3.4:1234/api/login",
json={"data": "login successful", "meta": {"rc": "ok"}},
headers={"content-type": "application/json"},
)
aioclient_mock.get(
"https://1.2.3.4:1234/api/self/sites",
json={
"data": [{"desc": "Site name", "name": "site_id"}],
"meta": {"rc": "ok"},
},
headers={"content-type": "application/json"},
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_HOST: "1.2.3.4",
CONF_USERNAME: "username",
CONF_PASSWORD: "password",
CONF_PORT: 1234,
CONF_VERIFY_SSL: True,
},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "no_local_user"
async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock):
@ -284,7 +335,7 @@ async def test_option_flow(hass):
CONF_TRACK_CLIENTS: False,
CONF_TRACK_WIRED_CLIENTS: False,
CONF_TRACK_DEVICES: False,
CONF_SSID_FILTER: ["SSID 1"],
CONF_SSID_FILTER: ["SSID 1", "SSID 2_IOT"],
CONF_DETECTION_TIME: 100,
},
)
@ -309,7 +360,7 @@ async def test_option_flow(hass):
CONF_TRACK_CLIENTS: False,
CONF_TRACK_WIRED_CLIENTS: False,
CONF_TRACK_DEVICES: False,
CONF_SSID_FILTER: ["SSID 1"],
CONF_SSID_FILTER: ["SSID 1", "SSID 2_IOT"],
CONF_DETECTION_TIME: 100,
CONF_IGNORE_WIRED_BUG: False,
CONF_POE_CLIENTS: False,

View File

@ -265,3 +265,11 @@ async def test_caching(hass):
await translation.async_get_translations(hass, "en", "state")
assert len(mock_merge.mock_calls) == 2
async def test_custom_component_translations(hass):
"""Test getting translation from custom components."""
hass.config.components.add("test_standalone")
hass.config.components.add("test_embedded")
hass.config.components.add("test_package")
assert await translation.async_get_translations(hass, "en", "state") == {}