Compare commits

..

26 Commits

Author SHA1 Message Date
Paulus Schoutsen
1333e23c23 Bumped version to 0.115.0b3 2020-09-09 13:15:53 +00:00
Martin Hjelmare
b572c0df7f Make spotify media class lookup more robust (#39841) 2020-09-09 13:15:47 +00:00
cgtobi
139a0ca008 Fix Kodi media browser (#39840)
* Refactor

* Make linter happy

* Only return at the end

* Handle exception
2020-09-09 13:15:46 +00:00
Chris Talkington
0458b5e3a6 Fix nzbget sensors (#39833)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-09-09 13:15:45 +00:00
cgtobi
c91c9f2b40 Fix Kodi media browser (#39829)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-09-09 13:15:45 +00:00
Maciej Bieniek
5165d746aa Add missing sensors after reworking sensor platform in Shelly integration (#39765) 2020-09-09 13:15:44 +00:00
Paulus Schoutsen
6cadc5b157 Bumped version to 0.115.0b2 2020-09-08 21:18:08 +00:00
Franchie
d32e3dc31a Avoid failing when hub does not provide cover position information (#39826)
The powerview hub, seemingly randomly, will occasionally not
provide data for cover positions. Some requests will return the
desired response, but minutes later the same request might not.

It appears this issue is being experienced by a number of people:
https://community.home-assistant.io/t/hunter-douglas-powerview-component-expanding-this-api/88635/48

While an unfortunate bug with the hub, crashing the integration
as a result of this missing data appears somewhat excessive.
This patch adds a simple check to ensure the 'position' key
has been returned by the hub before attempting to access its
data.
2020-09-08 21:18:04 +00:00
Bram Kragten
807bfb71df Update frontend to 20200908.0 (#39824) 2020-09-08 21:18:03 +00:00
Erik Montnemery
c2f16cf21d Fix MQTT light value template (#39820) 2020-09-08 21:18:02 +00:00
Paulus Schoutsen
9ca7efbe4c Bumped version to 0.115.0b1 2020-09-08 15:35:41 +00:00
J. Nick Koston
a5dec53e1b Fix isy994 send_node_command (#39806) 2020-09-08 15:35:35 +00:00
J. Nick Koston
f1de903fb5 Restore missing device_class to template binary_sensor (#39805) 2020-09-08 15:35:35 +00:00
J. Nick Koston
fa07787007 Fix cover template entities honoring titlecase True/False (#39803) 2020-09-08 15:35:34 +00:00
Emilv2
0d27e10d77 Bump pydelijn to 0.6.1 (#39802) 2020-09-08 15:35:33 +00:00
Franck Nijhof
8dee5f4cf8 Remove deprecated Hue configuration (#39800) 2020-09-08 15:35:32 +00:00
Paulus Schoutsen
c6a7350db1 Remove HTML support from frontend (#39799) 2020-09-08 15:35:32 +00:00
Paulus Schoutsen
2a68952334 Some shelly fixes (#39798) 2020-09-08 15:35:31 +00:00
Paulus Schoutsen
7f801faed1 Copy instead of deepcopy the variables in a wait for trigger (#39796) 2020-09-08 15:35:30 +00:00
cgtobi
02600bf190 Fix Sonos issue (#39790) 2020-09-08 15:35:29 +00:00
Bas Nijholt
f41d283354 Restore miflora now that v0.7.0 is out (#39787)
* add miflora again, reverts part of github.com/home-assistant/core/pull/37707

* edit CODEOWNERS
2020-09-08 15:35:29 +00:00
Franck Nijhof
f34e831650 Remove invalidation version from panel_custom (#39782) 2020-09-08 15:35:28 +00:00
J. Nick Koston
c9ec533aa5 Add the ability to reload bayesian platforms from yaml (#39771) 2020-09-08 15:35:27 +00:00
Martin Hjelmare
2a879afc7a Add media class browse media attribute (#39770) 2020-09-08 15:35:26 +00:00
Paulus Schoutsen
71c2557405 Guard for spotify items without type (#39795)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-09-08 15:35:03 +00:00
Simone Chemelli
214fc04473 Support shelly cover(roller) mode (#39711)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-09-08 15:32:48 +00:00
62 changed files with 848 additions and 577 deletions

View File

@@ -589,7 +589,6 @@ omit =
homeassistant/components/nut/sensor.py
homeassistant/components/nx584/alarm_control_panel.py
homeassistant/components/nzbget/coordinator.py
homeassistant/components/nzbget/sensor.py
homeassistant/components/obihai/*
homeassistant/components/octoprint/*
homeassistant/components/oem/climate.py
@@ -760,6 +759,7 @@ omit =
homeassistant/components/shodan/sensor.py
homeassistant/components/shelly/__init__.py
homeassistant/components/shelly/binary_sensor.py
homeassistant/components/shelly/cover.py
homeassistant/components/shelly/entity.py
homeassistant/components/shelly/light.py
homeassistant/components/shelly/sensor.py

View File

@@ -253,7 +253,7 @@ homeassistant/components/met/* @danielhiversen @thimic
homeassistant/components/meteo_france/* @hacf-fr @oncleben31 @Quentame
homeassistant/components/meteoalarm/* @rolfberkenbosch
homeassistant/components/metoffice/* @MrHarcombe
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel @basnijholt
homeassistant/components/mikrotik/* @engrbm87
homeassistant/components/mill/* @danielhiversen
homeassistant/components/min_max/* @fabaff

View File

@@ -7,6 +7,8 @@ from arcam.fmj.state import State
from homeassistant import config_entries
from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity
from homeassistant.components.media_player.const import (
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_MUSIC,
MEDIA_TYPE_MUSIC,
SUPPORT_BROWSE_MEDIA,
SUPPORT_PLAY_MEDIA,
@@ -255,6 +257,7 @@ class ArcamFmj(MediaPlayerEntity):
radio = [
BrowseMedia(
title=preset.name,
media_class=MEDIA_CLASS_MUSIC,
media_content_id=f"preset:{preset.index}",
media_content_type=MEDIA_TYPE_MUSIC,
can_play=True,
@@ -265,6 +268,7 @@ class ArcamFmj(MediaPlayerEntity):
root = BrowseMedia(
title="Root",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="root",
media_content_type="library",
can_play=False,

View File

@@ -1 +1,4 @@
"""The bayesian component."""
DOMAIN = "bayesian"
PLATFORMS = ["binary_sensor"]

View File

@@ -25,8 +25,11 @@ from homeassistant.helpers.event import (
async_track_state_change_event,
async_track_template_result,
)
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.template import result_as_boolean
from . import DOMAIN, PLATFORMS
ATTR_OBSERVATIONS = "observations"
ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities"
ATTR_PROBABILITY = "probability"
@@ -106,6 +109,8 @@ def update_probability(prior, prob_given_true, prob_given_false):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Bayesian Binary sensor."""
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
name = config[CONF_NAME]
observations = config[CONF_OBSERVATIONS]
prior = config[CONF_PRIOR]

View File

@@ -0,0 +1,2 @@
reload:
description: Reload all bayesian entities.

View File

@@ -3,5 +3,5 @@
"name": "De Lijn",
"documentation": "https://www.home-assistant.io/integrations/delijn",
"codeowners": ["@bollewolle", "@Emilv2"],
"requirements": ["pydelijn==0.6.0"]
"requirements": ["pydelijn==0.6.1"]
}

View File

@@ -70,8 +70,6 @@ MANIFEST_JSON = {
DATA_PANELS = "frontend_panels"
DATA_JS_VERSION = "frontend_js_version"
DATA_EXTRA_HTML_URL = "frontend_extra_html_url"
DATA_EXTRA_HTML_URL_ES5 = "frontend_extra_html_url_es5"
DATA_EXTRA_MODULE_URL = "frontend_extra_module_url"
DATA_EXTRA_JS_URL_ES5 = "frontend_extra_js_url_es5"
@@ -91,29 +89,23 @@ _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
cv.deprecated(CONF_EXTRA_HTML_URL, invalidation_version="0.115"),
cv.deprecated(CONF_EXTRA_HTML_URL_ES5, invalidation_version="0.115"),
vol.Schema(
{
vol.Optional(CONF_FRONTEND_REPO): cv.isdir,
vol.Optional(CONF_THEMES): vol.Schema(
{cv.string: {cv.string: cv.string}}
),
vol.Optional(CONF_EXTRA_HTML_URL): vol.All(
cv.ensure_list, [cv.string]
),
vol.Optional(CONF_EXTRA_MODULE_URL): vol.All(
cv.ensure_list, [cv.string]
),
vol.Optional(CONF_EXTRA_JS_URL_ES5): vol.All(
cv.ensure_list, [cv.string]
),
# We no longer use these options.
vol.Optional(CONF_EXTRA_HTML_URL_ES5): cv.match_all,
vol.Optional(CONF_JS_VERSION): cv.match_all,
},
),
DOMAIN: vol.Schema(
{
vol.Optional(CONF_FRONTEND_REPO): cv.isdir,
vol.Optional(CONF_THEMES): vol.Schema(
{cv.string: {cv.string: cv.string}}
),
vol.Optional(CONF_EXTRA_MODULE_URL): vol.All(
cv.ensure_list, [cv.string]
),
vol.Optional(CONF_EXTRA_JS_URL_ES5): vol.All(
cv.ensure_list, [cv.string]
),
# We no longer use these options.
vol.Optional(CONF_EXTRA_HTML_URL): cv.match_all,
vol.Optional(CONF_EXTRA_HTML_URL_ES5): cv.match_all,
vol.Optional(CONF_JS_VERSION): cv.match_all,
},
)
},
extra=vol.ALLOW_EXTRA,
@@ -220,17 +212,6 @@ def async_remove_panel(hass, frontend_url_path):
hass.bus.async_fire(EVENT_PANELS_UPDATED)
@bind_hass
@callback
def add_extra_html_url(hass, url, es5=False):
"""Register extra html url to load."""
key = DATA_EXTRA_HTML_URL_ES5 if es5 else DATA_EXTRA_HTML_URL
url_set = hass.data.get(key)
if url_set is None:
url_set = hass.data[key] = set()
url_set.add(url)
def add_extra_js_url(hass, url, es5=False):
"""Register extra js or module url to load."""
key = DATA_EXTRA_JS_URL_ES5 if es5 else DATA_EXTRA_MODULE_URL
@@ -267,6 +248,13 @@ async def async_setup(hass, config):
conf = config.get(DOMAIN, {})
for key in (CONF_EXTRA_HTML_URL, CONF_EXTRA_HTML_URL_ES5, CONF_JS_VERSION):
if key in conf:
_LOGGER.error(
"Please remove %s from your frontend config. It is no longer supported",
key,
)
repo_path = conf.get(CONF_FRONTEND_REPO)
is_dev = repo_path is not None
root_path = _frontend_root(repo_path)
@@ -315,12 +303,6 @@ async def async_setup(hass, config):
sidebar_icon="hass:hammer",
)
if DATA_EXTRA_HTML_URL not in hass.data:
hass.data[DATA_EXTRA_HTML_URL] = set()
for url in conf.get(CONF_EXTRA_HTML_URL, []):
add_extra_html_url(hass, url, False)
if DATA_EXTRA_MODULE_URL not in hass.data:
hass.data[DATA_EXTRA_MODULE_URL] = set()
@@ -522,7 +504,6 @@ class IndexView(web_urldispatcher.AbstractResource):
return web.Response(
text=template.render(
theme_color=MANIFEST_JSON["theme_color"],
extra_urls=hass.data[DATA_EXTRA_HTML_URL],
extra_modules=hass.data[DATA_EXTRA_MODULE_URL],
extra_js_es5=hass.data[DATA_EXTRA_JS_URL_ES5],
),

View File

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

View File

@@ -1,14 +1,11 @@
"""Support for the Philips Hue system."""
import ipaddress
import logging
from aiohue.util import normalize_bridge_id
import voluptuous as vol
from homeassistant import config_entries, core
from homeassistant.components import persistent_notification
from homeassistant.const import CONF_HOST
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers import device_registry as dr
from .bridge import HueBridge
from .const import (
@@ -21,80 +18,10 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
CONF_BRIDGES = "bridges"
DATA_CONFIGS = "hue_configs"
PHUE_CONFIG_FILE = "phue.conf"
BRIDGE_CONFIG_SCHEMA = vol.Schema(
{
# Validate as IP address and then convert back to a string.
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean,
vol.Optional(CONF_ALLOW_HUE_GROUPS): cv.boolean,
vol.Optional("filename"): str,
}
)
CONFIG_SCHEMA = vol.Schema(
vol.All(
cv.deprecated(DOMAIN, invalidation_version="0.115.0"),
{
DOMAIN: vol.Schema(
{
vol.Optional(CONF_BRIDGES): vol.All(
cv.ensure_list,
[BRIDGE_CONFIG_SCHEMA],
)
}
)
},
),
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
"""Set up the Hue platform."""
conf = config.get(DOMAIN)
if conf is None:
conf = {}
hass.data[DOMAIN] = {}
hass.data[DATA_CONFIGS] = {}
# User has not configured bridges
if CONF_BRIDGES not in conf:
return True
bridges = conf[CONF_BRIDGES]
configured_hosts = {
entry.data.get("host") for entry in hass.config_entries.async_entries(DOMAIN)
}
for bridge_conf in bridges:
host = bridge_conf[CONF_HOST]
# Store config in hass.data so the config entry can find it
hass.data[DATA_CONFIGS][host] = bridge_conf
if host in configured_hosts:
continue
# No existing config entry found, trigger link config flow. Because we're
# inside the setup of this component we'll have to use hass.async_add_job
# to avoid a deadlock: creating a config entry will set up the component
# but the setup would block till the entry is created!
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data={"host": bridge_conf[CONF_HOST]},
)
)
return True
@@ -102,8 +29,6 @@ async def async_setup_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
):
"""Set up a bridge from a config entry."""
host = entry.data["host"]
config = hass.data[DATA_CONFIGS].get(host)
# Migrate allow_unreachable from config entry data to config entry options
if (
@@ -133,27 +58,6 @@ async def async_setup_entry(
data.pop(CONF_ALLOW_HUE_GROUPS)
hass.config_entries.async_update_entry(entry, data=data, options=options)
# Overwrite from YAML configuration
if config is not None:
options = {}
if CONF_ALLOW_HUE_GROUPS in config and (
CONF_ALLOW_HUE_GROUPS not in entry.options
or config[CONF_ALLOW_HUE_GROUPS] != entry.options[CONF_ALLOW_HUE_GROUPS]
):
options[CONF_ALLOW_HUE_GROUPS] = config[CONF_ALLOW_HUE_GROUPS]
if CONF_ALLOW_UNREACHABLE in config and (
CONF_ALLOW_UNREACHABLE not in entry.options
or config[CONF_ALLOW_UNREACHABLE] != entry.options[CONF_ALLOW_UNREACHABLE]
):
options[CONF_ALLOW_UNREACHABLE] = config[CONF_ALLOW_UNREACHABLE]
if options:
hass.config_entries.async_update_entry(
entry,
options={**entry.options, **options},
)
bridge = HueBridge(hass, entry)
if not await bridge.async_setup():

View File

@@ -215,7 +215,7 @@ class PowerViewShade(ShadeEntity, CoverEntity):
def _async_update_current_cover_position(self):
"""Update the current cover position from the data."""
_LOGGER.debug("Raw data update: %s", self._shade.raw_data)
position_data = self._shade.raw_data[ATTR_POSITION_DATA]
position_data = self._shade.raw_data.get(ATTR_POSITION_DATA, {})
if ATTR_POSITION1 in position_data:
self._current_cover_position = position_data[ATTR_POSITION1]
self._is_opening = False

View File

@@ -49,7 +49,6 @@ from .const import (
)
from .entity import ISYNodeEntity, ISYProgramEntity
from .helpers import migrate_old_unique_ids
from .services import async_setup_device_services
DEVICE_PARENT_REQUIRED = [
DEVICE_CLASS_OPENING,
@@ -172,7 +171,6 @@ async def async_setup_entry(
await migrate_old_unique_ids(hass, BINARY_SENSOR, devices)
async_add_entities(devices)
async_setup_device_services(hass)
def _detect_device_type_and_class(node: Union[Group, Node]) -> (str, str):

View File

@@ -52,7 +52,6 @@ from .const import (
)
from .entity import ISYNodeEntity
from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids
from .services import async_setup_device_services
ISY_SUPPORTED_FEATURES = (
SUPPORT_FAN_MODE | SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE
@@ -73,7 +72,6 @@ async def async_setup_entry(
await migrate_old_unique_ids(hass, CLIMATE, entities)
async_add_entities(entities)
async_setup_device_services(hass)
class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):

View File

@@ -24,7 +24,6 @@ from .const import (
)
from .entity import ISYNodeEntity, ISYProgramEntity
from .helpers import migrate_old_unique_ids
from .services import async_setup_device_services
async def async_setup_entry(
@@ -43,7 +42,6 @@ async def async_setup_entry(
await migrate_old_unique_ids(hass, COVER, devices)
async_add_entities(devices)
async_setup_device_services(hass)
class ISYCoverEntity(ISYNodeEntity, CoverEntity):

View File

@@ -18,7 +18,6 @@ from homeassistant.helpers.typing import HomeAssistantType
from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS
from .entity import ISYNodeEntity, ISYProgramEntity
from .helpers import migrate_old_unique_ids
from .services import async_setup_device_services
VALUE_TO_STATE = {
0: SPEED_OFF,
@@ -51,7 +50,6 @@ async def async_setup_entry(
await migrate_old_unique_ids(hass, FAN, devices)
async_add_entities(devices)
async_setup_device_services(hass)
class ISYFanEntity(ISYNodeEntity, FanEntity):

View File

@@ -20,7 +20,7 @@ from .const import (
)
from .entity import ISYNodeEntity
from .helpers import migrate_old_unique_ids
from .services import async_setup_device_services, async_setup_light_services
from .services import async_setup_light_services
ATTR_LAST_BRIGHTNESS = "last_brightness"
@@ -41,7 +41,6 @@ async def async_setup_entry(
await migrate_old_unique_ids(hass, LIGHT, devices)
async_add_entities(devices)
async_setup_device_services(hass)
async_setup_light_services(hass)

View File

@@ -10,7 +10,6 @@ from homeassistant.helpers.typing import HomeAssistantType
from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS
from .entity import ISYNodeEntity, ISYProgramEntity
from .helpers import migrate_old_unique_ids
from .services import async_setup_device_services
VALUE_TO_STATE = {0: False, 100: True}
@@ -31,7 +30,6 @@ async def async_setup_entry(
await migrate_old_unique_ids(hass, LOCK, devices)
async_add_entities(devices)
async_setup_device_services(hass)
class ISYLockEntity(ISYNodeEntity, LockEntity):

View File

@@ -19,7 +19,6 @@ from .const import (
)
from .entity import ISYEntity, ISYNodeEntity
from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids
from .services import async_setup_device_services
async def async_setup_entry(
@@ -40,7 +39,6 @@ async def async_setup_entry(
await migrate_old_unique_ids(hass, SENSOR, devices)
async_add_entities(devices)
async_setup_device_services(hass)
class ISYSensorEntity(ISYNodeEntity):

View File

@@ -13,9 +13,10 @@ from homeassistant.const import (
CONF_UNIT_OF_MEASUREMENT,
SERVICE_RELOAD,
)
from homeassistant.core import callback
from homeassistant.core import ServiceCall, callback
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import async_get_platforms
import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.typing import HomeAssistantType
@@ -353,6 +354,30 @@ def async_setup_services(hass: HomeAssistantType):
domain=DOMAIN, service=SERVICE_RELOAD, service_func=async_reload_config_entries
)
async def _async_send_raw_node_command(call: ServiceCall):
await hass.helpers.service.entity_service_call(
async_get_platforms(hass, DOMAIN), SERVICE_SEND_RAW_NODE_COMMAND, call
)
hass.services.async_register(
domain=DOMAIN,
service=SERVICE_SEND_RAW_NODE_COMMAND,
schema=cv.make_entity_service_schema(SERVICE_SEND_RAW_NODE_COMMAND_SCHEMA),
service_func=_async_send_raw_node_command,
)
async def _async_send_node_command(call: ServiceCall):
await hass.helpers.service.entity_service_call(
async_get_platforms(hass, DOMAIN), SERVICE_SEND_NODE_COMMAND, call
)
hass.services.async_register(
domain=DOMAIN,
service=SERVICE_SEND_NODE_COMMAND,
schema=cv.make_entity_service_schema(SERVICE_SEND_NODE_COMMAND_SCHEMA),
service_func=_async_send_node_command,
)
@callback
def async_unload_services(hass: HomeAssistantType):
@@ -374,23 +399,8 @@ def async_unload_services(hass: HomeAssistantType):
hass.services.async_remove(domain=DOMAIN, service=SERVICE_SET_VARIABLE)
hass.services.async_remove(domain=DOMAIN, service=SERVICE_CLEANUP)
hass.services.async_remove(domain=DOMAIN, service=SERVICE_RELOAD)
@callback
def async_setup_device_services(hass: HomeAssistantType):
"""Create device-specific services for the ISY Integration."""
platform = entity_platform.current_platform.get()
platform.async_register_entity_service(
SERVICE_SEND_RAW_NODE_COMMAND,
SERVICE_SEND_RAW_NODE_COMMAND_SCHEMA,
SERVICE_SEND_RAW_NODE_COMMAND,
)
platform.async_register_entity_service(
SERVICE_SEND_NODE_COMMAND,
SERVICE_SEND_NODE_COMMAND_SCHEMA,
SERVICE_SEND_NODE_COMMAND,
)
hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_RAW_NODE_COMMAND)
hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_NODE_COMMAND)
@callback

View File

@@ -10,7 +10,6 @@ from homeassistant.helpers.typing import HomeAssistantType
from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS
from .entity import ISYNodeEntity, ISYProgramEntity
from .helpers import migrate_old_unique_ids
from .services import async_setup_device_services
async def async_setup_entry(
@@ -29,7 +28,6 @@ async def async_setup_entry(
await migrate_old_unique_ids(hass, SWITCH, devices)
async_add_entities(devices)
async_setup_device_services(hass)
class ISYSwitchEntity(ISYNodeEntity, SwitchEntity):

View File

@@ -1,7 +1,18 @@
"""Support for media browsing."""
import logging
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player import BrowseError, BrowseMedia
from homeassistant.components.media_player.const import (
MEDIA_CLASS_ALBUM,
MEDIA_CLASS_ARTIST,
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_EPISODE,
MEDIA_CLASS_MOVIE,
MEDIA_CLASS_MUSIC,
MEDIA_CLASS_PLAYLIST,
MEDIA_CLASS_SEASON,
MEDIA_CLASS_TRACK,
MEDIA_CLASS_TV_SHOW,
MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST,
MEDIA_TYPE_EPISODE,
@@ -18,13 +29,23 @@ PLAYABLE_MEDIA_TYPES = [
MEDIA_TYPE_TRACK,
]
EXPANDABLE_MEDIA_TYPES = [
MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST,
MEDIA_TYPE_PLAYLIST,
MEDIA_TYPE_TVSHOW,
MEDIA_TYPE_SEASON,
]
CONTENT_TYPE_MEDIA_CLASS = {
"library_music": MEDIA_CLASS_MUSIC,
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE,
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK,
MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW,
MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE,
}
_LOGGER = logging.getLogger(__name__)
class UnknownMediaType(BrowseError):
"""Unknown media type."""
async def build_item_response(media_library, payload):
@@ -121,15 +142,23 @@ async def build_item_response(media_library, payload):
title = season["seasondetails"]["label"]
if media is None:
return
return None
children = []
for item in media:
try:
children.append(item_payload(item, media_library))
except UnknownMediaType:
pass
return BrowseMedia(
media_content_id=payload["search_id"],
media_class=CONTENT_TYPE_MEDIA_CLASS[search_type],
media_content_id=search_id,
media_content_type=search_type,
title=title,
can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id,
can_expand=True,
children=[item_payload(item, media_library) for item in media],
children=children,
thumbnail=thumbnail,
)
@@ -140,43 +169,64 @@ def item_payload(item, media_library):
Used by async_browse_media.
"""
if "songid" in item:
media_content_type = MEDIA_TYPE_TRACK
media_content_id = f"{item['songid']}"
elif "albumid" in item:
media_content_type = MEDIA_TYPE_ALBUM
media_content_id = f"{item['albumid']}"
elif "artistid" in item:
media_content_type = MEDIA_TYPE_ARTIST
media_content_id = f"{item['artistid']}"
elif "movieid" in item:
media_content_type = MEDIA_TYPE_MOVIE
media_content_id = f"{item['movieid']}"
elif "episodeid" in item:
media_content_type = MEDIA_TYPE_EPISODE
media_content_id = f"{item['episodeid']}"
elif "seasonid" in item:
media_content_type = MEDIA_TYPE_SEASON
media_content_id = f"{item['tvshowid']}/{item['season']}"
elif "tvshowid" in item:
media_content_type = MEDIA_TYPE_TVSHOW
media_content_id = f"{item['tvshowid']}"
else:
# this case is for the top folder of each type
# possible content types: album, artist, movie, library_music, tvshow
media_content_type = item.get("type")
media_content_id = ""
title = item["label"]
can_play = media_content_type in PLAYABLE_MEDIA_TYPES and bool(media_content_id)
can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES
thumbnail = item.get("thumbnail")
if thumbnail:
thumbnail = media_library.thumbnail_url(thumbnail)
if "songid" in item:
media_content_type = MEDIA_TYPE_TRACK
media_content_id = f"{item['songid']}"
can_play = True
can_expand = False
elif "albumid" in item:
media_content_type = MEDIA_TYPE_ALBUM
media_content_id = f"{item['albumid']}"
can_play = True
can_expand = True
elif "artistid" in item:
media_content_type = MEDIA_TYPE_ARTIST
media_content_id = f"{item['artistid']}"
can_play = True
can_expand = True
elif "movieid" in item:
media_content_type = MEDIA_TYPE_MOVIE
media_content_id = f"{item['movieid']}"
can_play = True
can_expand = False
elif "episodeid" in item:
media_content_type = MEDIA_TYPE_EPISODE
media_content_id = f"{item['episodeid']}"
can_play = True
can_expand = False
elif "seasonid" in item:
media_content_type = MEDIA_TYPE_SEASON
media_content_id = f"{item['tvshowid']}/{item['season']}"
can_play = False
can_expand = True
elif "tvshowid" in item:
media_content_type = MEDIA_TYPE_TVSHOW
media_content_id = f"{item['tvshowid']}"
can_play = False
can_expand = True
else:
# this case is for the top folder of each type
# possible content types: album, artist, movie, library_music, tvshow
media_content_type = item["type"]
media_content_id = ""
can_play = False
can_expand = True
try:
media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type]
except KeyError as err:
_LOGGER.debug("Unknown media type received: %s", media_content_type)
raise UnknownMediaType from err
return BrowseMedia(
title=title,
media_class=media_class,
media_content_type=media_content_type,
media_content_id=media_content_id,
can_play=can_play,
@@ -192,6 +242,7 @@ def library_payload(media_library):
Used by async_browse_media.
"""
library_info = BrowseMedia(
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="library",
media_content_type="library",
title="Media Library",

View File

@@ -822,6 +822,7 @@ class MediaPlayerEntity(Entity):
Payload should follow this format:
{
"title": str - Title of the item
"media_class": str - Media class
"media_content_type": str - see below
"media_content_id": str - see below
- Can be passed back in to browse further
@@ -1046,6 +1047,7 @@ class BrowseMedia:
def __init__(
self,
*,
media_class: str,
media_content_id: str,
media_content_type: str,
title: str,
@@ -1055,6 +1057,7 @@ class BrowseMedia:
thumbnail: Optional[str] = None,
):
"""Initialize browse media item."""
self.media_class = media_class
self.media_content_id = media_content_id
self.media_content_type = media_content_type
self.title = title
@@ -1067,6 +1070,7 @@ class BrowseMedia:
"""Convert Media class to browse media dictionary."""
response = {
"title": self.title,
"media_class": self.media_class,
"media_content_type": self.media_content_type,
"media_content_id": self.media_content_id,
"can_play": self.can_play,

View File

@@ -29,6 +29,29 @@ ATTR_SOUND_MODE_LIST = "sound_mode_list"
DOMAIN = "media_player"
MEDIA_CLASS_ALBUM = "album"
MEDIA_CLASS_APP = "app"
MEDIA_CLASS_APPS = "apps"
MEDIA_CLASS_ARTIST = "artist"
MEDIA_CLASS_CHANNEL = "channel"
MEDIA_CLASS_CHANNELS = "channels"
MEDIA_CLASS_COMPOSER = "composer"
MEDIA_CLASS_CONTRIBUTING_ARTIST = "contributing_artist"
MEDIA_CLASS_DIRECTORY = "directory"
MEDIA_CLASS_EPISODE = "episode"
MEDIA_CLASS_GAME = "game"
MEDIA_CLASS_GENRE = "genre"
MEDIA_CLASS_IMAGE = "image"
MEDIA_CLASS_MOVIE = "movie"
MEDIA_CLASS_MUSIC = "music"
MEDIA_CLASS_PLAYLIST = "playlist"
MEDIA_CLASS_PODCAST = "podcast"
MEDIA_CLASS_SEASON = "season"
MEDIA_CLASS_TRACK = "track"
MEDIA_CLASS_TV_SHOW = "tv_show"
MEDIA_CLASS_URL = "url"
MEDIA_CLASS_VIDEO = "video"
MEDIA_TYPE_ALBUM = "album"
MEDIA_TYPE_APP = "app"
MEDIA_TYPE_APPS = "apps"

View File

@@ -6,6 +6,7 @@ from typing import Tuple
from aiohttp import web
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.media_source.error import Unresolvable
from homeassistant.core import HomeAssistant, callback
@@ -114,6 +115,7 @@ class LocalSource(MediaSource):
media = BrowseMediaSource(
domain=DOMAIN,
identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.path('media'))}",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_type="directory",
title=title,
can_play=is_file,

View File

@@ -5,6 +5,8 @@ from typing import List, Optional, Tuple
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import (
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_CHANNELS,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
)
@@ -52,6 +54,7 @@ class MediaSourceItem:
base = BrowseMediaSource(
domain=None,
identifier=None,
media_class=MEDIA_CLASS_CHANNELS,
media_content_type=MEDIA_TYPE_CHANNELS,
title="Media Sources",
can_play=False,
@@ -61,6 +64,7 @@ class MediaSourceItem:
BrowseMediaSource(
domain=source.domain,
identifier=None,
media_class=MEDIA_CLASS_CHANNEL,
media_content_type=MEDIA_TYPE_CHANNEL,
title=source.name,
can_play=False,

View File

@@ -1,8 +1,7 @@
{
"disabled": "Dependency contains code that breaks Home Assistant.",
"domain": "miflora",
"name": "Mi Flora",
"documentation": "https://www.home-assistant.io/integrations/miflora",
"requirements": ["bluepy==1.3.0", "miflora==0.6.0"],
"codeowners": ["@danielhiversen", "@ChristianKuehnel"]
"requirements": ["bluepy==1.3.0", "miflora==0.7.0"],
"codeowners": ["@danielhiversen", "@ChristianKuehnel", "@basnijholt"]
}

View File

@@ -35,6 +35,7 @@ from homeassistant.const import (
CONF_PAYLOAD_OFF,
CONF_PAYLOAD_ON,
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
STATE_ON,
)
from homeassistant.core import callback
@@ -157,6 +158,9 @@ async def async_setup_entity_basic(
hass, config, async_add_entities, config_entry, discovery_data=None
):
"""Set up a MQTT Light."""
if CONF_STATE_VALUE_TEMPLATE not in config and CONF_VALUE_TEMPLATE in config:
config[CONF_STATE_VALUE_TEMPLATE] = config[CONF_VALUE_TEMPLATE]
async_add_entities([MqttLight(hass, config, config_entry, discovery_data)])

View File

@@ -4,7 +4,10 @@ import logging
import re
from typing import Optional, Tuple
from homeassistant.components.media_player.const import MEDIA_TYPE_VIDEO
from homeassistant.components.media_player.const import (
MEDIA_CLASS_VIDEO,
MEDIA_TYPE_VIDEO,
)
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.media_source.const import MEDIA_MIME_TYPES
from homeassistant.components.media_source.error import MediaSourceError, Unresolvable
@@ -91,6 +94,7 @@ class NetatmoSource(MediaSource):
media = BrowseMediaSource(
domain=DOMAIN,
identifier=path,
media_class=MEDIA_CLASS_VIDEO,
media_content_type=MEDIA_TYPE_VIDEO,
title=title,
can_play=bool(

View File

@@ -61,7 +61,7 @@ async def async_setup_entry(
)
)
async_add_entities(sensors, True)
async_add_entities(sensors)
class NZBGetSensor(NZBGetEntity, Entity):
@@ -108,7 +108,7 @@ class NZBGetSensor(NZBGetEntity, Entity):
@property
def state(self):
"""Return the state of the sensor."""
value = self.coordinator.data.status.get(self._sensor_type)
value = self.coordinator.data["status"].get(self._sensor_type)
if value is None:
_LOGGER.warning("Unable to locate value for %s", self._sensor_type)

View File

@@ -1,6 +1,5 @@
"""Register a custom front end panel."""
import logging
import os
import voluptuous as vol
@@ -15,7 +14,6 @@ CONF_SIDEBAR_TITLE = "sidebar_title"
CONF_SIDEBAR_ICON = "sidebar_icon"
CONF_URL_PATH = "url_path"
CONF_CONFIG = "config"
CONF_WEBCOMPONENT_PATH = "webcomponent_path"
CONF_JS_URL = "js_url"
CONF_MODULE_URL = "module_url"
CONF_EMBED_IFRAME = "embed_iframe"
@@ -32,55 +30,34 @@ LEGACY_URL = "/api/panel_custom/{}"
PANEL_DIR = "panels"
def url_validator(value):
"""Validate required urls are specified."""
has_js_url = CONF_JS_URL in value
has_html_url = CONF_WEBCOMPONENT_PATH in value
has_module_url = CONF_MODULE_URL in value
if has_html_url and (has_js_url or has_module_url):
raise vol.Invalid("You cannot specify other urls besides a webcomponent path")
return value
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.All(
cv.ensure_list,
[
vol.All(
cv.deprecated(CONF_WEBCOMPONENT_PATH, invalidation_version="0.115"),
vol.Schema(
{
vol.Required(CONF_COMPONENT_NAME): cv.string,
vol.Optional(CONF_SIDEBAR_TITLE): cv.string,
vol.Optional(
CONF_SIDEBAR_ICON, default=DEFAULT_ICON
): cv.icon,
vol.Optional(CONF_URL_PATH): cv.string,
vol.Optional(CONF_CONFIG): dict,
vol.Optional(
CONF_WEBCOMPONENT_PATH,
): cv.string,
vol.Optional(
CONF_JS_URL,
): cv.string,
vol.Optional(
CONF_MODULE_URL,
): cv.string,
vol.Optional(
CONF_EMBED_IFRAME, default=DEFAULT_EMBED_IFRAME
): cv.boolean,
vol.Optional(
CONF_TRUST_EXTERNAL_SCRIPT,
default=DEFAULT_TRUST_EXTERNAL,
): cv.boolean,
vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean,
}
),
url_validator,
)
vol.Schema(
{
vol.Required(CONF_COMPONENT_NAME): cv.string,
vol.Optional(CONF_SIDEBAR_TITLE): cv.string,
vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon,
vol.Optional(CONF_URL_PATH): cv.string,
vol.Optional(CONF_CONFIG): dict,
vol.Optional(
CONF_JS_URL,
): cv.string,
vol.Optional(
CONF_MODULE_URL,
): cv.string,
vol.Optional(
CONF_EMBED_IFRAME, default=DEFAULT_EMBED_IFRAME
): cv.boolean,
vol.Optional(
CONF_TRUST_EXTERNAL_SCRIPT,
default=DEFAULT_TRUST_EXTERNAL,
): cv.boolean,
vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean,
}
),
],
)
},
@@ -98,8 +75,6 @@ async def async_register_panel(
# Title/icon for sidebar
sidebar_title=None,
sidebar_icon=None,
# HTML source of your panel
html_url=None,
# JS source of your panel
js_url=None,
# JS module of your panel
@@ -114,16 +89,11 @@ async def async_register_panel(
require_admin=False,
):
"""Register a new custom panel."""
if js_url is None and html_url is None and module_url is None:
if js_url is None and module_url is None:
raise ValueError("Either js_url, module_url or html_url is required.")
if html_url and (js_url or module_url):
raise ValueError("You cannot specify other paths with an HTML url")
if config is not None and not isinstance(config, dict):
raise ValueError("Config needs to be a dictionary.")
if html_url:
_LOGGER.warning("HTML custom panels have been deprecated")
custom_panel_config = {
"name": webcomponent_name,
"embed_iframe": embed_iframe,
@@ -136,9 +106,6 @@ async def async_register_panel(
if module_url is not None:
custom_panel_config["module_url"] = module_url
if html_url is not None:
custom_panel_config["html_url"] = html_url
if config is not None:
# Make copy because we're mutating it
config = dict(config)
@@ -162,8 +129,6 @@ async def async_setup(hass, config):
if DOMAIN not in config:
return True
seen = set()
for panel in config[DOMAIN]:
name = panel[CONF_COMPONENT_NAME]
@@ -184,29 +149,6 @@ async def async_setup(hass, config):
if CONF_MODULE_URL in panel:
kwargs["module_url"] = panel[CONF_MODULE_URL]
if CONF_MODULE_URL not in panel and CONF_JS_URL not in panel:
if name in seen:
_LOGGER.warning(
"Got HTML panel with duplicate name %s. Not registering", name
)
continue
seen.add(name)
panel_path = panel.get(CONF_WEBCOMPONENT_PATH)
if panel_path is None:
panel_path = hass.config.path(PANEL_DIR, f"{name}.html")
if not await hass.async_add_executor_job(os.path.isfile, panel_path):
_LOGGER.error(
"Unable to find webcomponent for %s: %s", name, panel_path
)
continue
url = LEGACY_URL.format(name)
hass.http.register_static_path(url, panel_path)
kwargs["html_url"] = url
try:
await async_register_panel(hass, **kwargs)
except ValueError as err:

View File

@@ -11,6 +11,8 @@ from homeassistant.components.media_player import (
MediaPlayerEntity,
)
from homeassistant.components.media_player.const import (
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_CHANNELS,
MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_CHANNELS,
SUPPORT_BROWSE_MEDIA,
@@ -288,6 +290,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
return BrowseMedia(
title="Channels",
media_class=MEDIA_CLASS_CHANNELS,
media_content_id="",
media_content_type=MEDIA_TYPE_CHANNELS,
can_play=False,
@@ -295,6 +298,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
children=[
BrowseMedia(
title=channel,
media_class=MEDIA_CLASS_CHANNEL,
media_content_id=channel,
media_content_type=MEDIA_TYPE_CHANNEL,
can_play=True,

View File

@@ -2,13 +2,31 @@
import logging
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import (
MEDIA_CLASS_ALBUM,
MEDIA_CLASS_ARTIST,
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_EPISODE,
MEDIA_CLASS_MOVIE,
MEDIA_CLASS_PLAYLIST,
MEDIA_CLASS_SEASON,
MEDIA_CLASS_TRACK,
MEDIA_CLASS_TV_SHOW,
MEDIA_CLASS_VIDEO,
)
from homeassistant.components.media_player.errors import BrowseError
from .const import DOMAIN
class UnknownMediaType(BrowseError):
"""Unknown media type."""
EXPANDABLES = ["album", "artist", "playlist", "season", "show"]
PLAYLISTS_BROWSE_PAYLOAD = {
"title": "Playlists",
"media_class": MEDIA_CLASS_PLAYLIST,
"media_content_id": "all",
"media_content_type": "playlists",
"can_play": False,
@@ -19,6 +37,18 @@ SPECIAL_METHODS = {
"Recently Added": "recentlyAdded",
}
ITEM_TYPE_MEDIA_CLASS = {
"album": MEDIA_CLASS_ALBUM,
"artist": MEDIA_CLASS_ARTIST,
"episode": MEDIA_CLASS_EPISODE,
"movie": MEDIA_CLASS_MOVIE,
"playlist": MEDIA_CLASS_PLAYLIST,
"season": MEDIA_CLASS_SEASON,
"show": MEDIA_CLASS_TV_SHOW,
"track": MEDIA_CLASS_TRACK,
"video": MEDIA_CLASS_VIDEO,
}
_LOGGER = logging.getLogger(__name__)
@@ -34,11 +64,17 @@ def browse_media(
if media is None:
return None
media_info = item_payload(media)
try:
media_info = item_payload(media)
except UnknownMediaType:
return None
if media_info.can_expand:
media_info.children = []
for item in media:
media_info.children.append(item_payload(item))
try:
media_info.children.append(item_payload(item))
except UnknownMediaType:
continue
return media_info
if media_content_id and ":" in media_content_id:
@@ -65,6 +101,7 @@ def browse_media(
payload = {
"title": title,
"media_class": MEDIA_CLASS_DIRECTORY,
"media_content_id": f"{media_content_id}:{special_folder}",
"media_content_type": media_content_type,
"can_play": False,
@@ -75,7 +112,10 @@ def browse_media(
method = SPECIAL_METHODS[special_folder]
items = getattr(library_or_section, method)()
for item in items:
payload["children"].append(item_payload(item))
try:
payload["children"].append(item_payload(item))
except UnknownMediaType:
continue
return BrowseMedia(**payload)
if media_content_type in ["server", None]:
@@ -99,8 +139,14 @@ def browse_media(
def item_payload(item):
"""Create response payload for a single media item."""
try:
media_class = ITEM_TYPE_MEDIA_CLASS[item.type]
except KeyError as err:
_LOGGER.debug("Unknown type received: %s", item.type)
raise UnknownMediaType from err
payload = {
"title": item.title,
"media_class": media_class,
"media_content_id": str(item.ratingKey),
"media_content_type": item.type,
"can_play": True,
@@ -116,6 +162,7 @@ def library_section_payload(section):
"""Create response payload for a single library section."""
return BrowseMedia(
title=section.title,
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id=section.key,
media_content_type="library",
can_play=False,
@@ -128,6 +175,7 @@ def special_library_payload(parent_payload, special_type):
title = f"{special_type} ({parent_payload.title})"
return BrowseMedia(
title=title,
media_class=parent_payload.media_class,
media_content_id=f"{parent_payload.media_content_id}:{special_type}",
media_content_type=parent_payload.media_content_type,
can_play=False,
@@ -139,6 +187,7 @@ def server_payload(plex_server):
"""Create response payload to describe libraries of the Plex server."""
server_info = BrowseMedia(
title=plex_server.friendly_name,
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id=plex_server.machine_identifier,
media_content_type="server",
can_play=False,
@@ -165,7 +214,10 @@ def library_payload(plex_server, library_id):
special_library_payload(library_info, "Recently Added")
)
for item in library.all():
library_info.children.append(item_payload(item))
try:
library_info.children.append(item_payload(item))
except UnknownMediaType:
continue
return library_info
@@ -173,5 +225,8 @@ def playlists_payload(plex_server):
"""Create response payload for all available playlists."""
playlists_info = {**PLAYLISTS_BROWSE_PAYLOAD, "children": []}
for playlist in plex_server.playlists():
playlists_info["children"].append(item_payload(playlist))
try:
playlists_info["children"].append(item_payload(playlist))
except UnknownMediaType:
continue
return BrowseMedia(**playlists_info)

View File

@@ -11,6 +11,11 @@ from homeassistant.components.media_player import (
MediaPlayerEntity,
)
from homeassistant.components.media_player.const import (
MEDIA_CLASS_APP,
MEDIA_CLASS_APPS,
MEDIA_CLASS_CHANNEL,
MEDIA_CLASS_CHANNELS,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_APP,
MEDIA_TYPE_APPS,
MEDIA_TYPE_CHANNEL,
@@ -79,6 +84,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia:
"""Create response payload to describe contents of a specific library."""
library_info = BrowseMedia(
title="Media Library",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="library",
media_content_type="library",
can_play=False,
@@ -89,6 +95,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia:
library_info.children.append(
BrowseMedia(
title="Apps",
media_class=MEDIA_CLASS_APPS,
media_content_id="apps",
media_content_type=MEDIA_TYPE_APPS,
can_expand=True,
@@ -100,6 +107,7 @@ def browse_media_library(channels: bool = False) -> BrowseMedia:
library_info.children.append(
BrowseMedia(
title="Channels",
media_class=MEDIA_CLASS_CHANNELS,
media_content_id="channels",
media_content_type=MEDIA_TYPE_CHANNELS,
can_expand=True,
@@ -286,6 +294,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
if media_content_type == MEDIA_TYPE_APPS:
response = BrowseMedia(
title="Apps",
media_class=MEDIA_CLASS_APPS,
media_content_id="apps",
media_content_type=MEDIA_TYPE_APPS,
can_expand=True,
@@ -294,6 +303,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
BrowseMedia(
title=app.name,
thumbnail=self.coordinator.roku.app_icon_url(app.app_id),
media_class=MEDIA_CLASS_APP,
media_content_id=app.app_id,
media_content_type=MEDIA_TYPE_APP,
can_play=True,
@@ -306,6 +316,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
if media_content_type == MEDIA_TYPE_CHANNELS:
response = BrowseMedia(
title="Channels",
media_class=MEDIA_CLASS_CHANNELS,
media_content_id="channels",
media_content_type=MEDIA_TYPE_CHANNELS,
can_expand=True,
@@ -313,6 +324,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
children=[
BrowseMedia(
title=channel.name,
media_class=MEDIA_CLASS_CHANNEL,
media_content_id=channel.number,
media_content_type=MEDIA_TYPE_CHANNEL,
can_play=True,

View File

@@ -20,7 +20,7 @@ from homeassistant.helpers import aiohttp_client, device_registry, update_coordi
from .const import DOMAIN
PLATFORMS = ["binary_sensor", "light", "sensor", "switch"]
PLATFORMS = ["binary_sensor", "cover", "light", "sensor", "switch"]
_LOGGER = logging.getLogger(__name__)

View File

@@ -3,6 +3,7 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASS_GAS,
DEVICE_CLASS_MOISTURE,
DEVICE_CLASS_OPENING,
DEVICE_CLASS_PROBLEM,
DEVICE_CLASS_SMOKE,
DEVICE_CLASS_VIBRATION,
BinarySensorEntity,
@@ -15,8 +16,18 @@ from .entity import (
)
SENSORS = {
("device", "overtemp"): BlockAttributeDescription(name="overtemp"),
("relay", "overpower"): BlockAttributeDescription(name="overpower"),
("device", "overtemp"): BlockAttributeDescription(
name="Overheating", device_class=DEVICE_CLASS_PROBLEM
),
("device", "overpower"): BlockAttributeDescription(
name="Over Power", device_class=DEVICE_CLASS_PROBLEM
),
("light", "overpower"): BlockAttributeDescription(
name="Over Power", device_class=DEVICE_CLASS_PROBLEM
),
("relay", "overpower"): BlockAttributeDescription(
name="Over Power", device_class=DEVICE_CLASS_PROBLEM
),
("sensor", "dwIsOpened"): BlockAttributeDescription(
name="Door", device_class=DEVICE_CLASS_OPENING
),

View File

@@ -0,0 +1,104 @@
"""Cover for Shelly."""
from aioshelly import Block
from homeassistant.components.cover import (
ATTR_POSITION,
SUPPORT_CLOSE,
SUPPORT_OPEN,
SUPPORT_SET_POSITION,
SUPPORT_STOP,
CoverEntity,
)
from homeassistant.core import callback
from . import ShellyDeviceWrapper
from .const import DOMAIN
from .entity import ShellyBlockEntity
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up cover for device."""
wrapper = hass.data[DOMAIN][config_entry.entry_id]
blocks = [block for block in wrapper.device.blocks if block.type == "roller"]
if not blocks:
return
async_add_entities(ShellyCover(wrapper, block) for block in blocks)
class ShellyCover(ShellyBlockEntity, CoverEntity):
"""Switch that controls a cover block on Shelly devices."""
def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None:
"""Initialize light."""
super().__init__(wrapper, block)
self.control_result = None
self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
if self.wrapper.device.settings["rollers"][0]["positioning"]:
self._supported_features |= SUPPORT_SET_POSITION
@property
def is_closed(self):
"""If cover is closed."""
if self.control_result:
return self.control_result["current_pos"] == 0
return self.block.rollerPos == 0
@property
def current_cover_position(self):
"""Position of the cover."""
if self.control_result:
return self.control_result["current_pos"]
return self.block.rollerPos
@property
def is_closing(self):
"""Return if the cover is closing."""
if self.control_result:
return self.control_result["state"] == "close"
return self.block.roller == "close"
@property
def is_opening(self):
"""Return if the cover is opening."""
if self.control_result:
return self.control_result["state"] == "open"
return self.block.roller == "open"
@property
def supported_features(self):
"""Flag supported features."""
return self._supported_features
async def async_close_cover(self, **kwargs):
"""Close cover."""
self.control_result = await self.block.set_state(go="close")
self.async_write_ha_state()
async def async_open_cover(self, **kwargs):
"""Open cover."""
self.control_result = await self.block.set_state(go="open")
self.async_write_ha_state()
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
self.control_result = await self.block.set_state(
go="to_pos", roller_pos=kwargs[ATTR_POSITION]
)
self.async_write_ha_state()
async def async_stop_cover(self, **_kwargs):
"""Stop the cover."""
self.control_result = await self.block.set_state(go="stop")
self.async_write_ha_state()
@callback
def _update_callback(self):
"""When device updates, clear control result that overrides state."""
self.control_result = None
super()._update_callback()

View File

@@ -2,7 +2,7 @@
"domain": "shelly",
"name": "Shelly",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/shelly2",
"documentation": "https://www.home-assistant.io/integrations/shelly",
"requirements": ["aioshelly==0.3.0"],
"zeroconf": ["_http._tcp.local."],
"codeowners": ["@balloob", "@bieniu"]

View File

@@ -40,6 +40,43 @@ SENSORS = {
device_class=sensor.DEVICE_CLASS_POWER,
default_enabled=False,
),
("device", "power"): BlockAttributeDescription(
name="Power",
unit=POWER_WATT,
value=lambda value: round(value, 1),
device_class=sensor.DEVICE_CLASS_POWER,
),
("emeter", "power"): BlockAttributeDescription(
name="Power",
unit=POWER_WATT,
value=lambda value: round(value, 1),
device_class=sensor.DEVICE_CLASS_POWER,
),
("relay", "power"): BlockAttributeDescription(
name="Power",
unit=POWER_WATT,
value=lambda value: round(value, 1),
device_class=sensor.DEVICE_CLASS_POWER,
),
("device", "energy"): BlockAttributeDescription(
name="Energy",
unit=ENERGY_KILO_WATT_HOUR,
value=lambda value: round(value / 60 / 1000, 2),
device_class=sensor.DEVICE_CLASS_ENERGY,
),
("emeter", "energy"): BlockAttributeDescription(
name="Energy",
unit=ENERGY_KILO_WATT_HOUR,
value=lambda value: round(value / 1000, 2),
device_class=sensor.DEVICE_CLASS_ENERGY,
),
("light", "energy"): BlockAttributeDescription(
name="Energy",
unit=ENERGY_KILO_WATT_HOUR,
value=lambda value: round(value / 60 / 1000, 2),
device_class=sensor.DEVICE_CLASS_ENERGY,
default_enabled=False,
),
("relay", "energy"): BlockAttributeDescription(
name="Energy",
unit=ENERGY_KILO_WATT_HOUR,

View File

@@ -17,6 +17,14 @@ import voluptuous as vol
from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity
from homeassistant.components.media_player.const import (
ATTR_MEDIA_ENQUEUE,
MEDIA_CLASS_ALBUM,
MEDIA_CLASS_ARTIST,
MEDIA_CLASS_COMPOSER,
MEDIA_CLASS_CONTRIBUTING_ARTIST,
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_GENRE,
MEDIA_CLASS_PLAYLIST,
MEDIA_CLASS_TRACK,
MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST,
MEDIA_TYPE_COMPOSER,
@@ -103,6 +111,23 @@ EXPANDABLE_MEDIA_TYPES = [
SONOS_PLAYLISTS,
]
SONOS_TO_MEDIA_CLASSES = {
SONOS_ALBUM: MEDIA_CLASS_ALBUM,
SONOS_ALBUM_ARTIST: MEDIA_CLASS_ARTIST,
SONOS_ARTIST: MEDIA_CLASS_CONTRIBUTING_ARTIST,
SONOS_COMPOSER: MEDIA_CLASS_COMPOSER,
SONOS_GENRE: MEDIA_CLASS_GENRE,
SONOS_PLAYLISTS: MEDIA_CLASS_PLAYLIST,
SONOS_TRACKS: MEDIA_CLASS_TRACK,
"object.container.album.musicAlbum": MEDIA_CLASS_ALBUM,
"object.container.genre.musicGenre": MEDIA_CLASS_PLAYLIST,
"object.container.person.composer": MEDIA_CLASS_PLAYLIST,
"object.container.person.musicArtist": MEDIA_CLASS_ARTIST,
"object.container.playlistContainer.sameArtist": MEDIA_CLASS_ARTIST,
"object.container.playlistContainer": MEDIA_CLASS_PLAYLIST,
"object.item.audioItem.musicTrack": MEDIA_CLASS_TRACK,
}
SONOS_TO_MEDIA_TYPES = {
SONOS_ALBUM: MEDIA_TYPE_ALBUM,
SONOS_ALBUM_ARTIST: MEDIA_TYPE_ARTIST,
@@ -1462,9 +1487,12 @@ def build_item_response(media_library, payload):
except IndexError:
title = LIBRARY_TITLES_MAPPING[payload["idstring"]]
media_class = SONOS_TO_MEDIA_CLASSES[MEDIA_TYPES_TO_SONOS[payload["search_type"]]]
return BrowseMedia(
title=title,
thumbnail=thumbnail,
media_class=media_class,
media_content_id=payload["idstring"],
media_content_type=payload["search_type"],
children=[item_payload(item) for item in media],
@@ -1482,6 +1510,7 @@ def item_payload(item):
return BrowseMedia(
title=item.title,
thumbnail=getattr(item, "album_art_uri", None),
media_class=SONOS_TO_MEDIA_CLASSES[get_media_type(item)],
media_content_id=get_content_id(item),
media_content_type=SONOS_TO_MEDIA_TYPES[get_media_type(item)],
can_play=can_play(item.item_class),
@@ -1497,6 +1526,7 @@ def library_payload(media_library):
"""
return BrowseMedia(
title="Music Library",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="library",
media_content_type="library",
can_play=False,
@@ -1565,6 +1595,7 @@ def get_media(media_library, item_id, search_type):
search_type,
"/".join(item_id.split("/")[:-1]),
full_album_art_uri=True,
max_items=0,
):
if item.item_id == item_id:
return item

View File

@@ -11,6 +11,13 @@ from yarl import URL
from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity
from homeassistant.components.media_player.const import (
MEDIA_CLASS_ALBUM,
MEDIA_CLASS_ARTIST,
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_EPISODE,
MEDIA_CLASS_PLAYLIST,
MEDIA_CLASS_PODCAST,
MEDIA_CLASS_TRACK,
MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST,
MEDIA_TYPE_EPISODE,
@@ -96,6 +103,35 @@ LIBRARY_MAP = {
"new_releases": "New Releases",
}
CONTENT_TYPE_MEDIA_CLASS = {
"current_user_playlists": MEDIA_CLASS_PLAYLIST,
"current_user_followed_artists": MEDIA_CLASS_ARTIST,
"current_user_saved_albums": MEDIA_CLASS_ALBUM,
"current_user_saved_tracks": MEDIA_CLASS_TRACK,
"current_user_saved_shows": MEDIA_CLASS_PODCAST,
"current_user_recently_played": MEDIA_CLASS_TRACK,
"current_user_top_artists": MEDIA_CLASS_ARTIST,
"current_user_top_tracks": MEDIA_CLASS_TRACK,
"featured_playlists": MEDIA_CLASS_PLAYLIST,
"categories": MEDIA_CLASS_DIRECTORY,
"category_playlists": MEDIA_CLASS_PLAYLIST,
"new_releases": MEDIA_CLASS_ALBUM,
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE,
MEDIA_TYPE_SHOW: MEDIA_CLASS_PODCAST,
MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK,
}
class MissingMediaInformation(BrowseError):
"""Missing media required information."""
class UnknownMediaType(BrowseError):
"""Unknown media type."""
async def async_setup_entry(
hass: HomeAssistant,
@@ -437,16 +473,16 @@ def build_item_response(spotify, user, payload):
items = media.get("artists", {}).get("items", [])
elif media_content_type == "current_user_saved_albums":
media = spotify.current_user_saved_albums(limit=BROWSE_LIMIT)
items = media.get("items", [])
items = [item["album"] for item in media.get("items", [])]
elif media_content_type == "current_user_saved_tracks":
media = spotify.current_user_saved_tracks(limit=BROWSE_LIMIT)
items = media.get("items", [])
items = [item["track"] for item in media.get("items", [])]
elif media_content_type == "current_user_saved_shows":
media = spotify.current_user_saved_shows(limit=BROWSE_LIMIT)
items = media.get("items", [])
items = [item["show"] for item in media.get("items", [])]
elif media_content_type == "current_user_recently_played":
media = spotify.current_user_recently_played(limit=BROWSE_LIMIT)
items = media.get("items", [])
items = [item["track"] for item in media.get("items", [])]
elif media_content_type == "current_user_top_artists":
media = spotify.current_user_top_artists(limit=BROWSE_LIMIT)
items = media.get("items", [])
@@ -474,7 +510,7 @@ def build_item_response(spotify, user, payload):
items = media.get("albums", {}).get("items", [])
elif media_content_type == MEDIA_TYPE_PLAYLIST:
media = spotify.playlist(media_content_id)
items = media.get("tracks", {}).get("items", [])
items = [item["track"] for item in media.get("tracks", {}).get("items", [])]
elif media_content_type == MEDIA_TYPE_ALBUM:
media = spotify.album(media_content_id)
items = media.get("tracks", {}).get("items", [])
@@ -497,25 +533,39 @@ def build_item_response(spotify, user, payload):
if media is None:
return None
try:
media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type]
except KeyError:
_LOGGER.debug("Unknown media type received: %s", media_content_type)
return None
if media_content_type == "categories":
return BrowseMedia(
media_item = BrowseMedia(
title=LIBRARY_MAP.get(media_content_id),
media_class=media_class,
media_content_id=media_content_id,
media_content_type=media_content_type,
can_play=False,
can_expand=True,
children=[
children=[],
)
for item in items:
try:
item_id = item["id"]
except KeyError:
_LOGGER.debug("Missing id for media item: %s", item)
continue
media_item.children.append(
BrowseMedia(
title=item.get("name"),
media_content_id=item["id"],
media_class=MEDIA_CLASS_PLAYLIST,
media_content_id=item_id,
media_content_type="category_playlists",
thumbnail=fetch_image_url(item, key="icons"),
can_play=False,
can_expand=True,
)
for item in items
],
)
)
if title is None:
if "name" in media:
@@ -525,12 +575,18 @@ def build_item_response(spotify, user, payload):
response = {
"title": title,
"media_class": media_class,
"media_content_id": media_content_id,
"media_content_type": media_content_type,
"can_play": media_content_type in PLAYABLE_MEDIA_TYPES,
"children": [item_payload(item) for item in items],
"children": [],
"can_expand": True,
}
for item in items:
try:
response["children"].append(item_payload(item))
except (MissingMediaInformation, UnknownMediaType):
continue
if "images" in media:
response["thumbnail"] = fetch_image_url(media)
@@ -546,28 +602,37 @@ def item_payload(item):
Used by async_browse_media.
"""
if MEDIA_TYPE_TRACK in item:
item = item[MEDIA_TYPE_TRACK]
elif MEDIA_TYPE_SHOW in item:
item = item[MEDIA_TYPE_SHOW]
elif MEDIA_TYPE_ARTIST in item:
item = item[MEDIA_TYPE_ARTIST]
elif MEDIA_TYPE_ALBUM in item and item["type"] != MEDIA_TYPE_TRACK:
item = item[MEDIA_TYPE_ALBUM]
try:
media_type = item["type"]
media_id = item["uri"]
except KeyError as err:
_LOGGER.debug("Missing type or uri for media item: %s", item)
raise MissingMediaInformation from err
can_expand = item["type"] not in [
try:
media_class = CONTENT_TYPE_MEDIA_CLASS[media_type]
except KeyError as err:
_LOGGER.debug("Unknown media type received: %s", media_type)
raise UnknownMediaType from err
can_expand = media_type not in [
MEDIA_TYPE_TRACK,
MEDIA_TYPE_EPISODE,
]
payload = {
"title": item.get("name"),
"media_content_id": item["uri"],
"media_content_type": item["type"],
"can_play": item["type"] in PLAYABLE_MEDIA_TYPES,
"media_content_id": media_id,
"media_content_type": media_type,
"can_play": media_type in PLAYABLE_MEDIA_TYPES,
"can_expand": can_expand,
}
payload = {
**payload,
"media_class": media_class,
}
if "images" in item:
payload["thumbnail"] = fetch_image_url(item)
elif MEDIA_TYPE_ALBUM in item:
@@ -584,6 +649,7 @@ def library_payload():
"""
library_info = {
"title": "Media Library",
"media_class": MEDIA_CLASS_DIRECTORY,
"media_content_id": "library",
"media_content_type": "library",
"can_play": False,

View File

@@ -194,3 +194,8 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity):
def is_on(self):
"""Return true if sensor is on."""
return self._state
@property
def device_class(self):
"""Return the sensor class of the binary sensor."""
return self._device_class

View File

@@ -249,15 +249,16 @@ class CoverTemplate(TemplateEntity, CoverEntity):
self._position = None
return
if result in _VALID_STATES:
if result in ("true", STATE_OPEN):
state = result.lower()
if state in _VALID_STATES:
if state in ("true", STATE_OPEN):
self._position = 100
else:
self._position = 0
else:
_LOGGER.error(
"Received invalid cover is_on state: %s. Expected: %s",
result,
state,
", ".join(_VALID_STATES),
)
self._position = None

View File

@@ -412,7 +412,7 @@ class LightTemplate(TemplateEntity, LightEntity):
self._available = True
return
state = str(result).lower()
state = result.lower()
if state in _VALID_STATES:
self._state = state in ("true", STATE_ON)
else:

View File

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

View File

@@ -595,3 +595,19 @@ class EntityPlatform:
current_platform: ContextVar[Optional[EntityPlatform]] = ContextVar(
"current_platform", default=None
)
@callback
def async_get_platforms(
hass: HomeAssistantType, integration_name: str
) -> List[EntityPlatform]:
"""Find existing platforms."""
if (
DATA_ENTITY_PLATFORM not in hass.data
or integration_name not in hass.data[DATA_ENTITY_PLATFORM]
):
return []
platforms: List[EntityPlatform] = hass.data[DATA_ENTITY_PLATFORM][integration_name]
return platforms

View File

@@ -9,7 +9,7 @@ from homeassistant.const import SERVICE_RELOAD
from homeassistant.core import Event, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform
from homeassistant.helpers.entity_platform import DATA_ENTITY_PLATFORM, EntityPlatform
from homeassistant.helpers.entity_platform import EntityPlatform, async_get_platforms
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import async_get_integration
from homeassistant.setup import async_setup_component
@@ -141,13 +141,7 @@ def async_get_platform(
hass: HomeAssistantType, integration_name: str, integration_platform_name: str
) -> Optional[EntityPlatform]:
"""Find an existing platform."""
if (
DATA_ENTITY_PLATFORM not in hass.data
or integration_name not in hass.data[DATA_ENTITY_PLATFORM]
):
return None
for integration_platform in hass.data[DATA_ENTITY_PLATFORM][integration_name]:
for integration_platform in async_get_platforms(hass, integration_name):
if integration_platform.domain == integration_platform_name:
platform: EntityPlatform = integration_platform
return platform

View File

@@ -1,6 +1,5 @@
"""Helpers to execute scripts."""
import asyncio
from copy import deepcopy
from datetime import datetime, timedelta
from functools import partial
import itertools
@@ -572,7 +571,7 @@ class _ScriptRun:
"" if delay is None else f" (timeout: {timedelta(seconds=delay)})",
)
variables = deepcopy(self._variables)
variables = {**self._variables}
self._variables["wait"] = {"remaining": delay, "trigger": None}
async def async_done(variables, context=None):

View File

@@ -13,7 +13,7 @@ defusedxml==0.6.0
distro==1.5.0
emoji==0.5.4
hass-nabucasa==0.36.1
home-assistant-frontend==20200907.0
home-assistant-frontend==20200908.0
importlib-metadata==1.6.0;python_version<'3.8'
jinja2>=2.11.2
netdisco==2.8.2

View File

@@ -363,6 +363,7 @@ blinkstick==1.1.8
blockchain==1.4.4
# homeassistant.components.decora
# homeassistant.components.miflora
# bluepy==1.3.0
# homeassistant.components.bme680
@@ -746,7 +747,7 @@ hole==0.5.1
holidays==0.10.3
# homeassistant.components.frontend
home-assistant-frontend==20200907.0
home-assistant-frontend==20200908.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -917,6 +918,9 @@ meteofrance-api==0.1.1
# homeassistant.components.mfi
mficlient==0.3.0
# homeassistant.components.miflora
miflora==0.7.0
# homeassistant.components.mill
millheater==0.3.4
@@ -1294,7 +1298,7 @@ pydanfossair==0.1.0
pydeconz==72
# homeassistant.components.delijn
pydelijn==0.6.0
pydelijn==0.6.1
# homeassistant.components.dexcom
pydexcom==0.2.0

View File

@@ -370,7 +370,7 @@ hole==0.5.1
holidays==0.10.3
# homeassistant.components.frontend
home-assistant-frontend==20200907.0
home-assistant-frontend==20200908.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10

View File

@@ -1,15 +1,18 @@
"""The test for the bayesian sensor platform."""
import json
from os import path
import unittest
from homeassistant.components.bayesian import binary_sensor as bayesian
from homeassistant import config as hass_config
from homeassistant.components.bayesian import DOMAIN, binary_sensor as bayesian
from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_RELOAD, STATE_UNKNOWN
from homeassistant.setup import async_setup_component, setup_component
from tests.async_mock import patch
from tests.common import get_test_home_assistant
@@ -631,3 +634,55 @@ async def test_monitored_sensor_goes_away(hass):
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.test_binary").state == "on"
async def test_reload(hass):
"""Verify we can reload bayesian sensors."""
config = {
"binary_sensor": {
"name": "test",
"platform": "bayesian",
"observations": [
{
"platform": "state",
"entity_id": "sensor.test_monitored",
"to_state": "on",
"prob_given_true": 0.9,
"prob_given_false": 0.4,
},
],
"prior": 0.2,
"probability_threshold": 0.32,
}
}
await async_setup_component(hass, "binary_sensor", config)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
assert hass.states.get("binary_sensor.test")
yaml_path = path.join(
_get_fixtures_base_path(),
"fixtures",
"bayesian/configuration.yaml",
)
with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path):
await hass.services.async_call(
DOMAIN,
SERVICE_RELOAD,
{},
blocking=True,
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
assert hass.states.get("binary_sensor.test") is None
assert hass.states.get("binary_sensor.test2")
def _get_fixtures_base_path():
return path.dirname(path.dirname(path.dirname(__file__)))

View File

@@ -334,14 +334,6 @@ async def test_missing_themes(hass, hass_ws_client):
assert msg["result"]["themes"] == {}
async def test_extra_urls(mock_http_client_with_urls, mock_onboarded):
"""Test that extra urls are loaded."""
resp = await mock_http_client_with_urls.get("/lovelace?latest")
assert resp.status == 200
text = await resp.text()
assert text.find('href="https://domain.com/my_extra_url.html"') >= 0
async def test_get_panels(hass, hass_ws_client, mock_http_client):
"""Test get_panels command."""
events = async_capture_events(hass, EVENT_PANELS_UPDATED)

View File

@@ -31,130 +31,6 @@ async def test_setup_with_no_config(hass):
assert hass.data[hue.DOMAIN] == {}
async def test_setup_defined_hosts_known_auth(hass):
"""Test we don't initiate a config entry if config bridge is known."""
MockConfigEntry(domain="hue", data={"host": "0.0.0.0"}).add_to_hass(hass)
with patch.object(hue, "async_setup_entry", return_value=True):
assert (
await async_setup_component(
hass,
hue.DOMAIN,
{
hue.DOMAIN: {
hue.CONF_BRIDGES: [
{
hue.CONF_HOST: "0.0.0.0",
hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True,
},
{hue.CONF_HOST: "1.1.1.1"},
]
}
},
)
is True
)
# Flow started for discovered bridge
assert len(hass.config_entries.flow.async_progress()) == 1
# Config stored for domain.
assert hass.data[hue.DATA_CONFIGS] == {
"0.0.0.0": {
hue.CONF_HOST: "0.0.0.0",
hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True,
},
"1.1.1.1": {hue.CONF_HOST: "1.1.1.1"},
}
async def test_setup_defined_hosts_no_known_auth(hass):
"""Test we initiate config entry if config bridge is not known."""
assert (
await async_setup_component(
hass,
hue.DOMAIN,
{
hue.DOMAIN: {
hue.CONF_BRIDGES: {
hue.CONF_HOST: "0.0.0.0",
hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True,
}
}
},
)
is True
)
# Flow started for discovered bridge
assert len(hass.config_entries.flow.async_progress()) == 1
# Config stored for domain.
assert hass.data[hue.DATA_CONFIGS] == {
"0.0.0.0": {
hue.CONF_HOST: "0.0.0.0",
hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True,
}
}
async def test_config_passed_to_config_entry(hass):
"""Test that configured options for a host are loaded via config entry."""
entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"})
entry.add_to_hass(hass)
mock_registry = Mock()
with patch.object(hue, "HueBridge") as mock_bridge, patch(
"homeassistant.helpers.device_registry.async_get_registry",
return_value=mock_registry,
):
mock_bridge.return_value.async_setup = AsyncMock(return_value=True)
mock_bridge.return_value.api.config = Mock(
mac="mock-mac",
bridgeid="mock-bridgeid",
modelid="mock-modelid",
swversion="mock-swversion",
)
# Can't set name via kwargs
mock_bridge.return_value.api.config.name = "mock-name"
assert (
await async_setup_component(
hass,
hue.DOMAIN,
{
hue.DOMAIN: {
hue.CONF_BRIDGES: {
hue.CONF_HOST: "0.0.0.0",
hue.CONF_ALLOW_HUE_GROUPS: False,
hue.CONF_ALLOW_UNREACHABLE: True,
}
}
},
)
is True
)
assert len(mock_bridge.mock_calls) == 2
p_hass, p_entry = mock_bridge.mock_calls[0][1]
assert p_hass is hass
assert p_entry is entry
assert len(mock_registry.mock_calls) == 1
assert mock_registry.mock_calls[0][2] == {
"config_entry_id": entry.entry_id,
"connections": {("mac", "mock-mac")},
"identifiers": {("hue", "mock-bridgeid")},
"manufacturer": "Signify",
"name": "mock-name",
"model": "mock-modelid",
"sw_version": "mock-swversion",
}
async def test_unload_entry(hass, mock_bridge_setup):
"""Test being able to unload an entry."""
entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"})

View File

@@ -2,6 +2,7 @@
import pytest
from homeassistant.components import media_source
from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY
from homeassistant.components.media_player.errors import BrowseError
from homeassistant.components.media_source import const
from homeassistant.setup import async_setup_component
@@ -77,6 +78,7 @@ async def test_websocket_browse_media(hass, hass_ws_client):
domain=const.DOMAIN,
identifier="/media",
title="Local Media",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_type="listing",
can_play=False,
can_expand=True,

View File

@@ -1,5 +1,9 @@
"""Test Media Source model methods."""
from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC
from homeassistant.components.media_player.const import (
MEDIA_CLASS_DIRECTORY,
MEDIA_CLASS_MUSIC,
MEDIA_TYPE_MUSIC,
)
from homeassistant.components.media_source import const, models
@@ -8,6 +12,7 @@ async def test_browse_media_as_dict():
base = models.BrowseMediaSource(
domain=const.DOMAIN,
identifier="media",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_type="folder",
title="media/",
can_play=False,
@@ -17,6 +22,7 @@ async def test_browse_media_as_dict():
models.BrowseMediaSource(
domain=const.DOMAIN,
identifier="media/test.mp3",
media_class=MEDIA_CLASS_MUSIC,
media_content_type=MEDIA_TYPE_MUSIC,
title="test.mp3",
can_play=True,
@@ -26,12 +32,14 @@ async def test_browse_media_as_dict():
item = base.as_dict()
assert item["title"] == "media/"
assert item["media_class"] == MEDIA_CLASS_DIRECTORY
assert item["media_content_type"] == "folder"
assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media"
assert not item["can_play"]
assert item["can_expand"]
assert len(item["children"]) == 1
assert item["children"][0]["title"] == "test.mp3"
assert item["children"][0]["media_class"] == MEDIA_CLASS_MUSIC
async def test_browse_media_parent_no_children():
@@ -39,6 +47,7 @@ async def test_browse_media_parent_no_children():
base = models.BrowseMediaSource(
domain=const.DOMAIN,
identifier="media",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_type="folder",
title="media/",
can_play=False,
@@ -47,6 +56,7 @@ async def test_browse_media_parent_no_children():
item = base.as_dict()
assert item["title"] == "media/"
assert item["media_class"] == MEDIA_CLASS_DIRECTORY
assert item["media_content_type"] == "folder"
assert item["media_content_id"] == f"{const.URI_SCHEME}{const.DOMAIN}/media"
assert not item["can_play"]

View File

@@ -648,6 +648,35 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock):
assert state.attributes.get("xy_color") == (0.14, 0.131)
async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock):
"""Test the setting of the state with undocumented value_template."""
config = {
light.DOMAIN: {
"platform": "mqtt",
"name": "test",
"state_topic": "test_light_rgb/status",
"command_topic": "test_light_rgb/set",
"value_template": "{{ value_json.hello }}",
}
}
assert await async_setup_component(hass, light.DOMAIN, config)
await hass.async_block_till_done()
state = hass.states.get("light.test")
assert state.state == STATE_OFF
async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "ON"}')
state = hass.states.get("light.test")
assert state.state == STATE_ON
async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "OFF"}')
state = hass.states.get("light.test")
assert state.state == STATE_OFF
async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock):
"""Test the sending of command in optimistic mode."""
config = {

View File

@@ -48,16 +48,16 @@ YAML_CONFIG = {
MOCK_VERSION = "21.0"
MOCK_STATUS = {
"ArticleCacheMB": "64",
"AverageDownloadRate": "512",
"DownloadPaused": "4",
"DownloadRate": "1000",
"DownloadedSizeMB": "256",
"FreeDiskSpaceMB": "1024",
"PostJobCount": "2",
"PostPaused": "4",
"RemainingSizeMB": "512",
"UpTimeSec": "600",
"ArticleCacheMB": 64,
"AverageDownloadRate": 1250000,
"DownloadPaused": 4,
"DownloadRate": 2500000,
"DownloadedSizeMB": 256,
"FreeDiskSpaceMB": 1024,
"PostJobCount": 2,
"PostPaused": 4,
"RemainingSizeMB": 512,
"UpTimeSec": 600,
}
MOCK_HISTORY = [

View File

@@ -0,0 +1,54 @@
"""Test the NZBGet sensors."""
from datetime import timedelta
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
DATA_MEGABYTES,
DATA_RATE_MEGABYTES_PER_SECOND,
DEVICE_CLASS_TIMESTAMP,
)
from homeassistant.util import dt as dt_util
from . import init_integration
from tests.async_mock import patch
async def test_sensors(hass) -> None:
"""Test the creation and values of the sensors."""
now = dt_util.utcnow().replace(microsecond=0)
with patch("homeassistant.util.dt.utcnow", return_value=now):
entry = await init_integration(hass)
registry = await hass.helpers.entity_registry.async_get_registry()
uptime = now - timedelta(seconds=600)
sensors = {
"article_cache": ("ArticleCacheMB", "64", DATA_MEGABYTES, None),
"average_speed": (
"AverageDownloadRate",
"1.19",
DATA_RATE_MEGABYTES_PER_SECOND,
None,
),
"download_paused": ("DownloadPaused", "4", None, None),
"speed": ("DownloadRate", "2.38", DATA_RATE_MEGABYTES_PER_SECOND, None),
"size": ("DownloadedSizeMB", "256", DATA_MEGABYTES, None),
"disk_free": ("FreeDiskSpaceMB", "1024", DATA_MEGABYTES, None),
"post_processing_jobs": ("PostJobCount", "2", "Jobs", None),
"post_processing_paused": ("PostPaused", "4", None, None),
"queue_size": ("RemainingSizeMB", "512", DATA_MEGABYTES, None),
"uptime": ("UpTimeSec", uptime.isoformat(), None, DEVICE_CLASS_TIMESTAMP),
}
for (sensor_id, data) in sensors.items():
entity_entry = registry.async_get(f"sensor.nzbgettest_{sensor_id}")
assert entity_entry
assert entity_entry.device_class == data[3]
assert entity_entry.unique_id == f"{entry.entry_id}_{data[0]}"
state = hass.states.get(f"sensor.nzbgettest_{sensor_id}")
assert state
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2]
assert state.state == data[1]

View File

@@ -30,52 +30,6 @@ async def test_webcomponent_custom_path_not_found(hass):
assert "nice_url" not in panels
async def test_webcomponent_custom_path(hass, caplog):
"""Test if a web component is found in config panels dir."""
filename = "mock.file"
config = {
"panel_custom": [
{
"name": "todo-mvc",
"webcomponent_path": filename,
"sidebar_title": "Sidebar Title",
"sidebar_icon": "mdi:iconicon",
"url_path": "nice_url",
"config": {"hello": "world"},
},
{"name": "todo-mvc"},
]
}
with patch("os.path.isfile", Mock(return_value=True)):
with patch("os.access", Mock(return_value=True)):
result = await setup.async_setup_component(hass, "panel_custom", config)
assert result
panels = hass.data.get(frontend.DATA_PANELS, [])
assert panels
assert "nice_url" in panels
panel = panels["nice_url"]
assert panel.config == {
"hello": "world",
"_panel_custom": {
"html_url": "/api/panel_custom/todo-mvc",
"name": "todo-mvc",
"embed_iframe": False,
"trust_external": False,
},
}
assert panel.frontend_url_path == "nice_url"
assert panel.sidebar_icon == "mdi:iconicon"
assert panel.sidebar_title == "Sidebar Title"
assert "Got HTML panel with duplicate name todo-mvc. Not registering" in caplog.text
async def test_js_webcomponent(hass):
"""Test if a web component is found in config panels dir."""
config = {
@@ -188,31 +142,6 @@ async def test_latest_and_es5_build(hass):
assert panel.frontend_url_path == "nice_url"
async def test_url_option_conflict(hass):
"""Test config with multiple url options."""
to_try = [
{
"panel_custom": {
"name": "todo-mvc",
"webcomponent_path": "/local/bla.html",
"js_url": "/local/bla.js",
}
},
{
"panel_custom": {
"name": "todo-mvc",
"webcomponent_path": "/local/bla.html",
"module_url": "/local/bla.js",
"js_url": "/local/bla.js",
}
},
]
for config in to_try:
result = await setup.async_setup_component(hass, "panel_custom", config)
assert not result
async def test_url_path_conflict(hass):
"""Test config with overlapping url path."""
assert await setup.async_setup_component(

View File

@@ -6,6 +6,7 @@ from unittest import mock
from homeassistant import setup
from homeassistant.const import (
ATTR_DEVICE_CLASS,
EVENT_HOMEASSISTANT_START,
STATE_OFF,
STATE_ON,
@@ -411,7 +412,10 @@ async def test_available_without_availability_template(hass):
await hass.async_start()
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE
state = hass.states.get("binary_sensor.test")
assert state.state != STATE_UNAVAILABLE
assert state.attributes[ATTR_DEVICE_CLASS] == "motion"
async def test_availability_template(hass):
@@ -443,7 +447,10 @@ async def test_availability_template(hass):
hass.states.async_set("sensor.test_state", STATE_ON)
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE
state = hass.states.get("binary_sensor.test")
assert state.state != STATE_UNAVAILABLE
assert state.attributes[ATTR_DEVICE_CLASS] == "motion"
async def test_invalid_attribute_template(hass, caplog):

View File

@@ -1079,3 +1079,44 @@ async def test_unique_id(hass):
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
async def test_state_gets_lowercased(hass):
"""Test True/False is lowercased."""
hass.states.async_set("binary_sensor.garage_door_sensor", "off")
await setup.async_setup_component(
hass,
"cover",
{
"cover": {
"platform": "template",
"covers": {
"garage_door": {
"friendly_name": "Garage Door",
"value_template": "{{ is_state('binary_sensor.garage_door_sensor', 'off') }}",
"open_cover": {
"service": "cover.open_cover",
"entity_id": "cover.test_state",
},
"close_cover": {
"service": "cover.close_cover",
"entity_id": "cover.test_state",
},
},
},
},
},
)
await hass.async_block_till_done()
await hass.async_start()
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 2
assert hass.states.get("cover.garage_door").state == STATE_OPEN
hass.states.async_set("binary_sensor.garage_door_sensor", "on")
await hass.async_block_till_done()
assert hass.states.get("cover.garage_door").state == STATE_CLOSED

View File

@@ -0,0 +1,10 @@
binary_sensor:
- platform: bayesian
prior: 0.1
observations:
- entity_id: 'switch.kitchen_lights'
prob_given_true: 0.6
prob_given_false: 0.2
platform: 'state'
to_state: 'on'
name: test2