forked from home-assistant/core
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1333e23c23 | ||
|
|
b572c0df7f | ||
|
|
139a0ca008 | ||
|
|
0458b5e3a6 | ||
|
|
c91c9f2b40 | ||
|
|
5165d746aa | ||
|
|
6cadc5b157 | ||
|
|
d32e3dc31a | ||
|
|
807bfb71df | ||
|
|
c2f16cf21d | ||
|
|
9ca7efbe4c | ||
|
|
a5dec53e1b | ||
|
|
f1de903fb5 | ||
|
|
fa07787007 | ||
|
|
0d27e10d77 | ||
|
|
8dee5f4cf8 | ||
|
|
c6a7350db1 | ||
|
|
2a68952334 | ||
|
|
7f801faed1 | ||
|
|
02600bf190 | ||
|
|
f41d283354 | ||
|
|
f34e831650 | ||
|
|
c9ec533aa5 | ||
|
|
2a879afc7a | ||
|
|
71c2557405 | ||
|
|
214fc04473 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
"""The bayesian component."""
|
||||
|
||||
DOMAIN = "bayesian"
|
||||
PLATFORMS = ["binary_sensor"]
|
||||
|
||||
@@ -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]
|
||||
|
||||
2
homeassistant/components/bayesian/services.yaml
Normal file
2
homeassistant/components/bayesian/services.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
reload:
|
||||
description: Reload all bayesian entities.
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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)])
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
104
homeassistant/components/shelly/cover.py
Normal file
104
homeassistant/components/shelly/cover.py
Normal 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()
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__)))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
54
tests/components/nzbget/test_sensor.py
Normal file
54
tests/components/nzbget/test_sensor.py
Normal 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]
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
10
tests/fixtures/bayesian/configuration.yaml
vendored
Normal file
10
tests/fixtures/bayesian/configuration.yaml
vendored
Normal 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
|
||||
Reference in New Issue
Block a user