mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
Merge pull request #35092 from home-assistant/rc
This commit is contained in:
commit
5f267be1db
@ -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
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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"]
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()}
|
||||
|
@ -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):
|
||||
|
@ -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]:
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"],
|
||||
|
@ -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": [
|
||||
|
@ -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]:
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
):
|
||||
|
@ -22,5 +22,9 @@ class LoginRequired(UnifiException):
|
||||
"""Component got logged out."""
|
||||
|
||||
|
||||
class NoLocalUser(UnifiException):
|
||||
"""No local user."""
|
||||
|
||||
|
||||
class UserLevel(UnifiException):
|
||||
"""User level too low."""
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
@ -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": {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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") == {}
|
||||
|
Loading…
x
Reference in New Issue
Block a user