mirror of
https://github.com/home-assistant/core.git
synced 2026-01-04 14:55:39 +01:00
Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddbadb1e26 | ||
|
|
cf81a5c09a | ||
|
|
a8b6464d7f | ||
|
|
85ae63c656 | ||
|
|
68cc34df6f | ||
|
|
9955e7e5e1 | ||
|
|
ab8ef1c9e1 | ||
|
|
7e6d64a24c | ||
|
|
e582caccc9 | ||
|
|
1eb8035122 | ||
|
|
57b7ed6a07 | ||
|
|
d35f06ac15 | ||
|
|
bf741c1b26 | ||
|
|
2df709c7d0 | ||
|
|
e9b355bd8a | ||
|
|
ef279b125d | ||
|
|
152b380a2f | ||
|
|
8a39bea761 | ||
|
|
d37fe1fbb6 | ||
|
|
33b56b0cf9 | ||
|
|
0383030266 | ||
|
|
b8fe0c6c3a | ||
|
|
7cb0c98c03 | ||
|
|
58c6702080 | ||
|
|
f77b3d4714 | ||
|
|
f5aee6b886 | ||
|
|
6f26722f69 | ||
|
|
13cfd1bae1 | ||
|
|
5271a3eb1e | ||
|
|
78022bf145 | ||
|
|
99a57f5a4e | ||
|
|
a9e220c96b | ||
|
|
f7d7765d5e | ||
|
|
9ffcf35b23 | ||
|
|
d3a59652bb | ||
|
|
c62a6cd779 | ||
|
|
f1169120ae | ||
|
|
b28dbe20b6 | ||
|
|
8dde59be02 | ||
|
|
abca177894 | ||
|
|
d3bb2e5e16 | ||
|
|
7f8a89838b | ||
|
|
39c4b338f1 | ||
|
|
4518335a56 | ||
|
|
b856b0e15d | ||
|
|
5d518b5365 | ||
|
|
ce86112612 | ||
|
|
e095120023 | ||
|
|
3ef3d848f7 | ||
|
|
610a327b52 | ||
|
|
81436fb688 | ||
|
|
24fe9cdd5a | ||
|
|
e5c499c22e | ||
|
|
99a8604601 | ||
|
|
3ef821d62f | ||
|
|
a38e047e83 | ||
|
|
e0fcf9b648 | ||
|
|
0e823b566b | ||
|
|
a9d24c2cd5 | ||
|
|
7a7cad39eb | ||
|
|
1a76a953c7 | ||
|
|
db27079fa8 | ||
|
|
ef1649383c | ||
|
|
afde5a7ece | ||
|
|
30b8565548 | ||
|
|
a971b92899 | ||
|
|
4ee7cdc8a0 | ||
|
|
4c2788a13c | ||
|
|
8b4e193614 | ||
|
|
f0ce65af7d | ||
|
|
b81c61dd99 | ||
|
|
30ef7a5e88 | ||
|
|
5a6492b76d | ||
|
|
b19fe17e76 | ||
|
|
47326b2295 | ||
|
|
951c373110 | ||
|
|
b9b76b3519 | ||
|
|
da6885af6c | ||
|
|
bc2173747c | ||
|
|
d0e6b3e268 | ||
|
|
172a02a605 | ||
|
|
b6f868f629 | ||
|
|
5697f4b4e7 | ||
|
|
30f9e1b479 | ||
|
|
fcbcebea9b | ||
|
|
f81606cbf5 | ||
|
|
3240be0bb6 | ||
|
|
18be6cbadc | ||
|
|
a002e9b12f | ||
|
|
db64a9ebfa | ||
|
|
3fbde22cc4 | ||
|
|
758e60a58d | ||
|
|
5201410e39 | ||
|
|
b1b7944012 | ||
|
|
8ef04268be | ||
|
|
b107e87d38 | ||
|
|
b0b9579778 | ||
|
|
7eade4029a | ||
|
|
3d4913348a | ||
|
|
1720b71d62 | ||
|
|
589086f0d0 | ||
|
|
6f8060dea7 | ||
|
|
b8ef87d84c | ||
|
|
7370b0ffc6 | ||
|
|
209cf44e8e | ||
|
|
b7dacabbe4 | ||
|
|
5098c35814 | ||
|
|
896df60f32 | ||
|
|
b26ab2849b | ||
|
|
36f52a26f6 | ||
|
|
f0295d562d | ||
|
|
081bd22e59 | ||
|
|
668c73010a | ||
|
|
fe371f0438 | ||
|
|
be28dc0bca | ||
|
|
4578baca3e | ||
|
|
6d7dfc0804 | ||
|
|
c5cf95c14b | ||
|
|
f79ce7bd04 | ||
|
|
578c1b283a | ||
|
|
5ae0844f35 | ||
|
|
8e3e2d436e | ||
|
|
4af6804c50 |
@@ -656,11 +656,6 @@ omit =
|
||||
homeassistant/components/plaato/*
|
||||
homeassistant/components/plex/media_player.py
|
||||
homeassistant/components/plex/sensor.py
|
||||
homeassistant/components/plugwise/__init__.py
|
||||
homeassistant/components/plugwise/binary_sensor.py
|
||||
homeassistant/components/plugwise/climate.py
|
||||
homeassistant/components/plugwise/sensor.py
|
||||
homeassistant/components/plugwise/switch.py
|
||||
homeassistant/components/plum_lightpad/light.py
|
||||
homeassistant/components/pocketcasts/sensor.py
|
||||
homeassistant/components/point/*
|
||||
|
||||
@@ -236,6 +236,7 @@ homeassistant/components/linux_battery/* @fabaff
|
||||
homeassistant/components/local_ip/* @issacg
|
||||
homeassistant/components/logger/* @home-assistant/core
|
||||
homeassistant/components/logi_circle/* @evanjd
|
||||
homeassistant/components/loopenergy/* @pavoni
|
||||
homeassistant/components/lovelace/* @home-assistant/frontend
|
||||
homeassistant/components/luci/* @fbradyirl @mzdrale
|
||||
homeassistant/components/luftdaten/* @fabaff
|
||||
@@ -465,7 +466,7 @@ homeassistant/components/velbus/* @Cereal2nd @brefra
|
||||
homeassistant/components/velux/* @Julius2342
|
||||
homeassistant/components/vera/* @vangorra
|
||||
homeassistant/components/versasense/* @flamm3blemuff1n
|
||||
homeassistant/components/version/* @fabaff
|
||||
homeassistant/components/version/* @fabaff @ludeeus
|
||||
homeassistant/components/vesync/* @markperdue @webdjoe @thegardenmonkey
|
||||
homeassistant/components/vicare/* @oischinger
|
||||
homeassistant/components/vilfo/* @ManneW
|
||||
|
||||
@@ -47,8 +47,9 @@ jobs:
|
||||
- template: templates/azp-job-wheels.yaml@azure
|
||||
parameters:
|
||||
builderVersion: '$(versionWheels)'
|
||||
builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev'
|
||||
builderApk: 'build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev'
|
||||
builderPip: 'Cython;numpy;scikit-build'
|
||||
builderEnvFile: true
|
||||
skipBinary: 'aiohttp'
|
||||
wheelsRequirement: 'requirements_wheels.txt'
|
||||
wheelsRequirementDiff: 'requirements_diff.txt'
|
||||
@@ -90,4 +91,10 @@ jobs:
|
||||
sed -i "s|# bme680|bme680|g" ${requirement_file}
|
||||
sed -i "s|# python-gammu|python-gammu|g" ${requirement_file}
|
||||
done
|
||||
|
||||
# Write env for build settings
|
||||
(
|
||||
echo "GRPC_BUILD_WITH_BORING_SSL_ASM="
|
||||
echo "GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1"
|
||||
) > .env_file
|
||||
displayName: 'Prepare requirements files for Home Assistant wheels'
|
||||
|
||||
10
build.json
10
build.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"image": "homeassistant/{arch}-homeassistant",
|
||||
"build_from": {
|
||||
"aarch64": "homeassistant/aarch64-homeassistant-base:8.3.0",
|
||||
"armhf": "homeassistant/armhf-homeassistant-base:8.3.0",
|
||||
"armv7": "homeassistant/armv7-homeassistant-base:8.3.0",
|
||||
"amd64": "homeassistant/amd64-homeassistant-base:8.3.0",
|
||||
"i386": "homeassistant/i386-homeassistant-base:8.3.0"
|
||||
"aarch64": "homeassistant/aarch64-homeassistant-base:8.4.0",
|
||||
"armhf": "homeassistant/armhf-homeassistant-base:8.4.0",
|
||||
"armv7": "homeassistant/armv7-homeassistant-base:8.4.0",
|
||||
"amd64": "homeassistant/amd64-homeassistant-base:8.4.0",
|
||||
"i386": "homeassistant/i386-homeassistant-base:8.4.0"
|
||||
},
|
||||
"labels": {
|
||||
"io.hass.type": "core"
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"abort": {
|
||||
"already_setup": "You can only configure one Almond account.",
|
||||
"cannot_connect": "Unable to connect to the Almond server.",
|
||||
"missing_configuration": "Please check the documentation on how to set up Almond."
|
||||
"missing_configuration": "Please check the documentation on how to set up Almond.",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
||||
"requirements": [
|
||||
"adb-shell[async]==0.2.1",
|
||||
"androidtv[async]==0.0.49",
|
||||
"androidtv[async]==0.0.50",
|
||||
"pure-python-adb[async]==0.3.0.dev0"
|
||||
],
|
||||
"codeowners": ["@JeffLIrion"]
|
||||
|
||||
@@ -380,7 +380,7 @@ def adb_decorator(override_available=False):
|
||||
# An unforeseen exception occurred. Close the ADB connection so that
|
||||
# it doesn't happen over and over again, then raise the exception.
|
||||
await self.aftv.adb_close()
|
||||
self._available = False # pylint: disable=protected-access
|
||||
self._available = False
|
||||
raise
|
||||
|
||||
return _adb_exception_catcher
|
||||
|
||||
@@ -29,8 +29,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
def get_service(hass, config, discovery_info=None):
|
||||
"""Get the Apprise notification service."""
|
||||
|
||||
# Create our object
|
||||
a_obj = apprise.Apprise()
|
||||
# Create our Apprise Asset Object
|
||||
asset = apprise.AppriseAsset(async_mode=False)
|
||||
|
||||
# Create our Apprise Instance (reference our asset)
|
||||
a_obj = apprise.Apprise(asset=asset)
|
||||
|
||||
if config.get(CONF_FILE):
|
||||
# Sourced from a Configuration File
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.const import (
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_PLATFORM,
|
||||
CONF_VARIABLES,
|
||||
CONF_ZONE,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
SERVICE_RELOAD,
|
||||
@@ -29,7 +30,7 @@ from homeassistant.core import (
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import condition, extract_domain_configs
|
||||
from homeassistant.helpers import condition, extract_domain_configs, template
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@@ -44,6 +45,7 @@ from homeassistant.helpers.script import (
|
||||
Script,
|
||||
make_script_schema,
|
||||
)
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.helpers.trigger import async_initialize_triggers
|
||||
from homeassistant.helpers.typing import TemplateVarsType
|
||||
@@ -104,6 +106,7 @@ PLATFORM_SCHEMA = vol.All(
|
||||
vol.Optional(CONF_HIDE_ENTITY): cv.boolean,
|
||||
vol.Required(CONF_TRIGGER): cv.TRIGGER_SCHEMA,
|
||||
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
|
||||
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
|
||||
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
},
|
||||
SCRIPT_MODE_SINGLE,
|
||||
@@ -239,6 +242,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
cond_func,
|
||||
action_script,
|
||||
initial_state,
|
||||
variables,
|
||||
):
|
||||
"""Initialize an automation entity."""
|
||||
self._id = automation_id
|
||||
@@ -253,6 +257,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
self._referenced_entities: Optional[Set[str]] = None
|
||||
self._referenced_devices: Optional[Set[str]] = None
|
||||
self._logger = _LOGGER
|
||||
self._variables: ScriptVariables = variables
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -378,11 +383,20 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
else:
|
||||
await self.async_disable()
|
||||
|
||||
async def async_trigger(self, variables, context=None, skip_condition=False):
|
||||
async def async_trigger(self, run_variables, context=None, skip_condition=False):
|
||||
"""Trigger automation.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if self._variables:
|
||||
try:
|
||||
variables = self._variables.async_render(self.hass, run_variables)
|
||||
except template.TemplateError as err:
|
||||
self._logger.error("Error rendering variables: %s", err)
|
||||
return
|
||||
else:
|
||||
variables = run_variables
|
||||
|
||||
if (
|
||||
not skip_condition
|
||||
and self._cond_func is not None
|
||||
@@ -518,6 +532,9 @@ async def _async_process_config(hass, config, component):
|
||||
max_runs=config_block[CONF_MAX],
|
||||
max_exceeded=config_block[CONF_MAX_EXCEEDED],
|
||||
logger=_LOGGER,
|
||||
# We don't pass variables here
|
||||
# Automation will already render them to use them in the condition
|
||||
# and so will pass them on to the script.
|
||||
)
|
||||
|
||||
if CONF_CONDITION in config_block:
|
||||
@@ -535,6 +552,7 @@ async def _async_process_config(hass, config, component):
|
||||
cond_func,
|
||||
action_script,
|
||||
initial_state,
|
||||
config_block.get(CONF_VARIABLES),
|
||||
)
|
||||
|
||||
entities.append(entity)
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/axis",
|
||||
"requirements": ["axis==35"],
|
||||
"zeroconf": ["_axis-video._tcp.local."],
|
||||
"zeroconf": [
|
||||
{"type":"_axis-video._tcp.local.","macaddress":"00408C*"},
|
||||
{"type":"_axis-video._tcp.local.","macaddress":"ACCC8E*"},
|
||||
{"type":"_axis-video._tcp.local.","macaddress":"B8A44F*"}
|
||||
],
|
||||
"after_dependencies": ["mqtt"],
|
||||
"codeowners": ["@Kane610"]
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ class BayesianBinarySensor(BinarySensorEntity):
|
||||
entity = event.data.get("entity_id")
|
||||
|
||||
self.current_observations.update(self._record_entity_observations(entity))
|
||||
self.async_set_context(event.context)
|
||||
self._recalculate_and_write_state()
|
||||
|
||||
self.async_on_remove(
|
||||
@@ -220,6 +221,8 @@ class BayesianBinarySensor(BinarySensorEntity):
|
||||
obs_entry = None
|
||||
self.current_observations[obs["id"]] = obs_entry
|
||||
|
||||
if event:
|
||||
self.async_set_context(event.context)
|
||||
self._recalculate_and_write_state()
|
||||
|
||||
for template in self.observations_by_template:
|
||||
|
||||
@@ -11,7 +11,7 @@ from broadlink.exceptions import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
@@ -20,6 +20,7 @@ from .const import ( # pylint: disable=unused-import
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_TIMEOUT,
|
||||
DOMAIN,
|
||||
DOMAINS_AND_TYPES,
|
||||
)
|
||||
from .helpers import format_mac
|
||||
|
||||
@@ -36,6 +37,19 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_set_device(self, device, raise_on_progress=True):
|
||||
"""Define a device for the config flow."""
|
||||
supported_types = {
|
||||
device_type
|
||||
for _, device_types in DOMAINS_AND_TYPES
|
||||
for device_type in device_types
|
||||
}
|
||||
if device.type not in supported_types:
|
||||
LOGGER.error(
|
||||
"Unsupported device: %s. If it worked before, please open "
|
||||
"an issue at https://github.com/home-assistant/core/issues",
|
||||
hex(device.devtype),
|
||||
)
|
||||
raise data_entry_flow.AbortFlow("not_supported")
|
||||
|
||||
await self.async_set_unique_id(
|
||||
device.mac.hex(), raise_on_progress=raise_on_progress
|
||||
)
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"already_in_progress": "There is already a configuration flow in progress for this device",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_host": "Invalid hostname or IP address",
|
||||
"not_supported": "Device not supported",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"already_in_progress": "There is already a configuration flow in progress for this device",
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_host": "Invalid hostname or IP address",
|
||||
"not_supported": "Device not supported",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""Support for fetching data from Broadlink devices."""
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import broadlink as blk
|
||||
from broadlink.exceptions import (
|
||||
AuthorizationError,
|
||||
BroadlinkException,
|
||||
CommandNotSupportedError,
|
||||
DeviceOfflineError,
|
||||
StorageError,
|
||||
)
|
||||
|
||||
@@ -18,6 +21,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def get_update_manager(device):
|
||||
"""Return an update manager for a given Broadlink device."""
|
||||
if device.api.model.startswith("RM mini"):
|
||||
return BroadlinkRMMini3UpdateManager(device)
|
||||
|
||||
update_managers = {
|
||||
"A1": BroadlinkA1UpdateManager,
|
||||
"MP1": BroadlinkMP1UpdateManager,
|
||||
@@ -95,6 +101,22 @@ class BroadlinkMP1UpdateManager(BroadlinkUpdateManager):
|
||||
return await self.device.async_request(self.device.api.check_power)
|
||||
|
||||
|
||||
class BroadlinkRMMini3UpdateManager(BroadlinkUpdateManager):
|
||||
"""Manages updates for Broadlink RM mini 3 devices."""
|
||||
|
||||
async def async_fetch_data(self):
|
||||
"""Fetch data from the device."""
|
||||
hello = partial(
|
||||
blk.discover,
|
||||
discover_ip_address=self.device.api.host[0],
|
||||
timeout=self.device.api.timeout,
|
||||
)
|
||||
devices = await self.device.hass.async_add_executor_job(hello)
|
||||
if not devices:
|
||||
raise DeviceOfflineError("The device is offline")
|
||||
return {}
|
||||
|
||||
|
||||
class BroadlinkRMUpdateManager(BroadlinkUpdateManager):
|
||||
"""Manages updates for Broadlink RM2 and RM4 devices."""
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/brother",
|
||||
"codeowners": ["@bieniu"],
|
||||
"requirements": ["brother==0.1.17"],
|
||||
"zeroconf": ["_printer._tcp.local."],
|
||||
"zeroconf": [{"type": "_printer._tcp.local.", "name":"brother*"}],
|
||||
"config_flow": true,
|
||||
"quality_scale": "platinum"
|
||||
}
|
||||
|
||||
@@ -375,9 +375,9 @@ class CastDevice(MediaPlayerEntity):
|
||||
if tts_base_url and media_status.content_id.startswith(tts_base_url):
|
||||
url_description = f" from tts.base_url ({tts_base_url})"
|
||||
if external_url and media_status.content_id.startswith(external_url):
|
||||
url_description = " from external_url ({external_url})"
|
||||
url_description = f" from external_url ({external_url})"
|
||||
if internal_url and media_status.content_id.startswith(internal_url):
|
||||
url_description = " from internal_url ({internal_url})"
|
||||
url_description = f" from internal_url ({internal_url})"
|
||||
|
||||
_LOGGER.error(
|
||||
"Failed to cast media %s%s. Please make sure the URL is: "
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "cloud",
|
||||
"name": "Home Assistant Cloud",
|
||||
"documentation": "https://www.home-assistant.io/integrations/cloud",
|
||||
"requirements": ["hass-nabucasa==0.36.1"],
|
||||
"requirements": ["hass-nabucasa==0.37.0"],
|
||||
"dependencies": ["http", "webhook", "alexa"],
|
||||
"after_dependencies": ["google_assistant"],
|
||||
"codeowners": ["@home-assistant/cloud"]
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"name": "CoolMasterNet",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/coolmaster",
|
||||
"requirements": ["pycoolmasternet-async==0.1.1"],
|
||||
"requirements": ["pycoolmasternet-async==0.1.2"],
|
||||
"codeowners": ["@OnFreund"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "deCONZ",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
||||
"requirements": ["pydeconz==72"],
|
||||
"requirements": ["pydeconz==73"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics"
|
||||
|
||||
@@ -27,7 +27,7 @@ async def async_setup_entry(
|
||||
|
||||
for device in hass.data[DOMAIN]["homecontrol"].multi_level_switch_devices:
|
||||
for multi_level_switch in device.multi_level_switch_property:
|
||||
if device.deviceModelUID in [
|
||||
if device.device_model_uid in [
|
||||
"devolo.model.Thermostat:Valve",
|
||||
"devolo.model.Room:Thermostat",
|
||||
]:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/doorbird",
|
||||
"requirements": ["doorbirdpy==2.1.0"],
|
||||
"dependencies": ["http"],
|
||||
"zeroconf": ["_axis-video._tcp.local."],
|
||||
"zeroconf": [{"type":"_axis-video._tcp.local.","macaddress":"1CCAE3*"}],
|
||||
"codeowners": ["@oblogic7", "@bdraco"],
|
||||
"config_flow": true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Support for displaying weather info from Ecobee API."""
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
from pyecobee.const import ECOBEE_STATE_UNKNOWN
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.components.weather import (
|
||||
WeatherEntity,
|
||||
)
|
||||
from homeassistant.const import TEMP_FAHRENHEIT
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
@@ -165,10 +166,13 @@ class EcobeeWeather(WeatherEntity):
|
||||
return None
|
||||
|
||||
forecasts = []
|
||||
for day in range(1, 5):
|
||||
date = dt_util.utcnow()
|
||||
for day in range(0, 5):
|
||||
forecast = _process_forecast(self.weather["forecasts"][day])
|
||||
if forecast is None:
|
||||
continue
|
||||
forecast[ATTR_FORECAST_TIME] = date.isoformat()
|
||||
date += timedelta(days=1)
|
||||
forecasts.append(forecast)
|
||||
|
||||
if forecasts:
|
||||
@@ -186,9 +190,6 @@ def _process_forecast(json):
|
||||
"""Process a single ecobee API forecast to return expected values."""
|
||||
forecast = {}
|
||||
try:
|
||||
forecast[ATTR_FORECAST_TIME] = datetime.strptime(
|
||||
json["dateTime"], "%Y-%m-%d %H:%M:%S"
|
||||
).isoformat()
|
||||
forecast[ATTR_FORECAST_CONDITION] = ECOBEE_WEATHER_SYMBOL_TO_HASS[
|
||||
json["weatherSymbol"]
|
||||
]
|
||||
|
||||
@@ -66,6 +66,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
|
||||
host = entry.data[CONF_HOST]
|
||||
port = entry.data[CONF_PORT]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
device_id = None
|
||||
|
||||
zeroconf_instance = await zeroconf.async_get_instance(hass)
|
||||
|
||||
@@ -129,6 +130,15 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
|
||||
"Can only generate events under esphome domain! (%s)", host
|
||||
)
|
||||
return
|
||||
|
||||
# Call native tag scan
|
||||
if service_name == "tag_scanned":
|
||||
tag_id = service_data["tag_id"]
|
||||
hass.async_create_task(
|
||||
hass.components.tag.async_scan_tag(tag_id, device_id)
|
||||
)
|
||||
return
|
||||
|
||||
hass.bus.async_fire(service.service, service_data)
|
||||
else:
|
||||
hass.async_create_task(
|
||||
@@ -166,10 +176,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
|
||||
|
||||
async def on_login() -> None:
|
||||
"""Subscribe to states and list entities on successful API login."""
|
||||
nonlocal device_id
|
||||
try:
|
||||
entry_data.device_info = await cli.device_info()
|
||||
entry_data.available = True
|
||||
await _async_setup_device_registry(hass, entry, entry_data.device_info)
|
||||
device_id = await _async_setup_device_registry(
|
||||
hass, entry, entry_data.device_info
|
||||
)
|
||||
entry_data.async_update_device_state(hass)
|
||||
|
||||
entity_infos, services = await cli.list_entities_services()
|
||||
@@ -265,7 +278,7 @@ async def _async_setup_device_registry(
|
||||
if device_info.compilation_time:
|
||||
sw_version += f" ({device_info.compilation_time})"
|
||||
device_registry = await dr.async_get_registry(hass)
|
||||
device_registry.async_get_or_create(
|
||||
entry = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)},
|
||||
name=device_info.name,
|
||||
@@ -273,6 +286,7 @@ async def _async_setup_device_registry(
|
||||
model=device_info.model,
|
||||
sw_version=sw_version,
|
||||
)
|
||||
return entry.id
|
||||
|
||||
|
||||
async def _register_service(
|
||||
|
||||
@@ -6,7 +6,5 @@
|
||||
"requirements": ["aioesphomeapi==2.6.3"],
|
||||
"zeroconf": ["_esphomelib._tcp.local."],
|
||||
"codeowners": ["@OttoWinter"],
|
||||
"after_dependencies": [
|
||||
"zeroconf"
|
||||
]
|
||||
"after_dependencies": ["zeroconf", "tag"]
|
||||
}
|
||||
|
||||
@@ -185,7 +185,9 @@ def request_app_setup(hass, config, add_entities, config_path, discovery_info=No
|
||||
else:
|
||||
setup_platform(hass, config, add_entities, discovery_info)
|
||||
|
||||
start_url = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}"
|
||||
start_url = (
|
||||
f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}"
|
||||
)
|
||||
|
||||
description = f"""Please create a Fitbit developer app at
|
||||
https://dev.fitbit.com/apps/new.
|
||||
@@ -220,7 +222,7 @@ def request_oauth_completion(hass):
|
||||
def fitbit_configuration_callback(callback_data):
|
||||
"""Handle configuration updates."""
|
||||
|
||||
start_url = f"{get_url(hass)}{FITBIT_AUTH_START}"
|
||||
start_url = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_START}"
|
||||
|
||||
description = f"Please authorize Fitbit by visiting {start_url}"
|
||||
|
||||
@@ -312,7 +314,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
config_file.get(CONF_CLIENT_ID), config_file.get(CONF_CLIENT_SECRET)
|
||||
)
|
||||
|
||||
redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}"
|
||||
redirect_uri = (
|
||||
f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}"
|
||||
)
|
||||
|
||||
fitbit_auth_start_url, _ = oauth.authorize_token_url(
|
||||
redirect_uri=redirect_uri,
|
||||
@@ -357,7 +361,7 @@ class FitbitAuthCallbackView(HomeAssistantView):
|
||||
|
||||
result = None
|
||||
if data.get("code") is not None:
|
||||
redirect_uri = f"{get_url(hass)}{FITBIT_AUTH_CALLBACK_PATH}"
|
||||
redirect_uri = f"{get_url(hass, require_current_request=True)}{FITBIT_AUTH_CALLBACK_PATH}"
|
||||
|
||||
try:
|
||||
result = self.oauth.fetch_access_token(data.get("code"), redirect_uri)
|
||||
|
||||
@@ -146,11 +146,12 @@ class FreeboxCallSensor(FreeboxSensor):
|
||||
def async_update_state(self) -> None:
|
||||
"""Update the Freebox call sensor."""
|
||||
self._call_list_for_type = []
|
||||
for call in self._router.call_list:
|
||||
if not call["new"]:
|
||||
continue
|
||||
if call["type"] == self._sensor_type:
|
||||
self._call_list_for_type.append(call)
|
||||
if self._router.call_list:
|
||||
for call in self._router.call_list:
|
||||
if not call["new"]:
|
||||
continue
|
||||
if call["type"] == self._sensor_type:
|
||||
self._call_list_for_type.append(call)
|
||||
|
||||
self._state = len(self._call_list_for_type)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20200908.0"],
|
||||
"requirements": ["home-assistant-frontend==20200918.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Gogogate2 and iSmartGate",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/gogogate2",
|
||||
"requirements": ["gogogate2-api==2.0.1"],
|
||||
"requirements": ["gogogate2-api==2.0.2"],
|
||||
"codeowners": ["@vangorra"],
|
||||
"homekit": {
|
||||
"models": [
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/hangouts",
|
||||
"requirements": [
|
||||
"hangups==0.4.10"
|
||||
"hangups==0.4.11"
|
||||
],
|
||||
"codeowners": []
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"missing_configuration": "The Home Connect component is not configured. Please follow the documentation."
|
||||
"missing_configuration": "The Home Connect component is not configured. Please follow the documentation.",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Successfully authenticated with Home Connect."
|
||||
|
||||
@@ -35,15 +35,15 @@ def _convert_states(states):
|
||||
"""Convert state definitions to State objects."""
|
||||
result = {}
|
||||
|
||||
for entity_id in states:
|
||||
for entity_id, info in states.items():
|
||||
entity_id = cv.entity_id(entity_id)
|
||||
|
||||
if isinstance(states[entity_id], dict):
|
||||
entity_attrs = states[entity_id].copy()
|
||||
if isinstance(info, dict):
|
||||
entity_attrs = info.copy()
|
||||
state = entity_attrs.pop(ATTR_STATE, None)
|
||||
attributes = entity_attrs
|
||||
else:
|
||||
state = states[entity_id]
|
||||
state = info
|
||||
attributes = {}
|
||||
|
||||
# YAML translates 'on' to a boolean
|
||||
|
||||
@@ -28,11 +28,15 @@ async def async_attach_trigger(
|
||||
):
|
||||
"""Listen for events based on configuration."""
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
event_data_schema = (
|
||||
vol.Schema(config.get(CONF_EVENT_DATA), extra=vol.ALLOW_EXTRA)
|
||||
if config.get(CONF_EVENT_DATA)
|
||||
else None
|
||||
)
|
||||
event_data_schema = None
|
||||
if config.get(CONF_EVENT_DATA):
|
||||
event_data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(key): value
|
||||
for key, value in config.get(CONF_EVENT_DATA).items()
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
@callback
|
||||
def handle_event(event):
|
||||
|
||||
@@ -80,6 +80,13 @@ async def async_attach_trigger(
|
||||
else:
|
||||
new_value = to_s.attributes.get(attribute)
|
||||
|
||||
# When we listen for state changes with `match_all`, we
|
||||
# will trigger even if just an attribute changes. When
|
||||
# we listen to just an attribute, we should ignore all
|
||||
# other attribute changes.
|
||||
if attribute is not None and old_value == new_value:
|
||||
return
|
||||
|
||||
if (
|
||||
not match_from_state(old_value)
|
||||
or not match_to_state(new_value)
|
||||
|
||||
@@ -38,7 +38,7 @@ from homeassistant.helpers import device_registry, entity_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entityfilter import BASE_FILTER_SCHEMA, FILTER_SCHEMA
|
||||
from homeassistant.helpers.reload import async_integration_yaml_config
|
||||
from homeassistant.loader import async_get_integration
|
||||
from homeassistant.loader import IntegrationNotFound, async_get_integration
|
||||
from homeassistant.util import get_local_ip
|
||||
|
||||
from .accessories import get_accessory
|
||||
@@ -712,8 +712,13 @@ class HomeKit:
|
||||
if dev_reg_ent.sw_version:
|
||||
ent_cfg[ATTR_SOFTWARE_VERSION] = dev_reg_ent.sw_version
|
||||
if ATTR_MANUFACTURER not in ent_cfg:
|
||||
integration = await async_get_integration(self.hass, ent_reg_ent.platform)
|
||||
ent_cfg[ATTR_INTERGRATION] = integration.name
|
||||
try:
|
||||
integration = await async_get_integration(
|
||||
self.hass, ent_reg_ent.platform
|
||||
)
|
||||
ent_cfg[ATTR_INTERGRATION] = integration.name
|
||||
except IntegrationNotFound:
|
||||
ent_cfg[ATTR_INTERGRATION] = ent_reg_ent.platform
|
||||
|
||||
|
||||
class HomeKitPairingQRView(HomeAssistantView):
|
||||
|
||||
@@ -24,6 +24,10 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_mired_to_kelvin,
|
||||
color_temperature_to_hs,
|
||||
)
|
||||
|
||||
from .accessories import TYPES, HomeAccessory
|
||||
from .const import (
|
||||
@@ -64,8 +68,6 @@ class Light(HomeAccessory):
|
||||
if self._features & SUPPORT_COLOR:
|
||||
self.chars.append(CHAR_HUE)
|
||||
self.chars.append(CHAR_SATURATION)
|
||||
self._hue = None
|
||||
self._saturation = None
|
||||
elif self._features & SUPPORT_COLOR_TEMP:
|
||||
# ColorTemperature and Hue characteristic should not be
|
||||
# exposed both. Both states are tracked separately in HomeKit,
|
||||
@@ -179,7 +181,16 @@ class Light(HomeAccessory):
|
||||
|
||||
# Handle Color
|
||||
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
|
||||
hue, saturation = new_state.attributes.get(ATTR_HS_COLOR, (None, None))
|
||||
if ATTR_HS_COLOR in new_state.attributes:
|
||||
hue, saturation = new_state.attributes[ATTR_HS_COLOR]
|
||||
elif ATTR_COLOR_TEMP in new_state.attributes:
|
||||
hue, saturation = color_temperature_to_hs(
|
||||
color_temperature_mired_to_kelvin(
|
||||
new_state.attributes[ATTR_COLOR_TEMP]
|
||||
)
|
||||
)
|
||||
else:
|
||||
hue, saturation = None, None
|
||||
if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)):
|
||||
hue = round(hue, 0)
|
||||
saturation = round(saturation, 0)
|
||||
|
||||
@@ -8,12 +8,19 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
async_get_registry as async_get_device_registry,
|
||||
)
|
||||
|
||||
from .connection import get_accessory_name, get_bridge_information
|
||||
from .const import DOMAIN, KNOWN_DEVICES
|
||||
|
||||
HOMEKIT_IGNORE = ["Home Assistant Bridge"]
|
||||
HOMEKIT_DIR = ".homekit"
|
||||
HOMEKIT_BRIDGE_DOMAIN = "homekit"
|
||||
HOMEKIT_BRIDGE_SERIAL_NUMBER = "homekit.bridge"
|
||||
HOMEKIT_BRIDGE_MODEL = "Home Assistant HomeKit Bridge"
|
||||
|
||||
PAIRING_FILE = "pairing.json"
|
||||
|
||||
PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$")
|
||||
@@ -141,6 +148,17 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
|
||||
|
||||
return self.async_abort(reason="no_devices")
|
||||
|
||||
async def _hkid_is_homekit_bridge(self, hkid):
|
||||
"""Determine if the device is a homekit bridge."""
|
||||
dev_reg = await async_get_device_registry(self.hass)
|
||||
device = dev_reg.async_get_device(
|
||||
identifiers=set(), connections={(CONNECTION_NETWORK_MAC, hkid)}
|
||||
)
|
||||
|
||||
if device is None:
|
||||
return False
|
||||
return device.model == HOMEKIT_BRIDGE_MODEL
|
||||
|
||||
async def async_step_zeroconf(self, discovery_info):
|
||||
"""Handle a discovered HomeKit accessory.
|
||||
|
||||
@@ -153,6 +171,12 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
|
||||
key.lower(): value for (key, value) in discovery_info["properties"].items()
|
||||
}
|
||||
|
||||
if "id" not in properties:
|
||||
_LOGGER.warning(
|
||||
"HomeKit device %s: id not exposed, in violation of spec", properties
|
||||
)
|
||||
return self.async_abort(reason="invalid_properties")
|
||||
|
||||
# The hkid is a unique random number that looks like a pairing code.
|
||||
# It changes if a device is factory reset.
|
||||
hkid = properties["id"]
|
||||
@@ -208,7 +232,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
|
||||
# Devices in HOMEKIT_IGNORE have native local integrations - users
|
||||
# should be encouraged to use native integration and not confused
|
||||
# by alternative HK API.
|
||||
if model in HOMEKIT_IGNORE:
|
||||
if await self._hkid_is_homekit_bridge(hkid):
|
||||
return self.async_abort(reason="ignored_model")
|
||||
|
||||
self.model = model
|
||||
@@ -280,9 +304,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow):
|
||||
# Its possible that the first try may have been busy so
|
||||
# we always check to see if self.finish_paring has been
|
||||
# set.
|
||||
discovery = await self.controller.find_ip_by_device_id(self.hkid)
|
||||
|
||||
try:
|
||||
discovery = await self.controller.find_ip_by_device_id(self.hkid)
|
||||
self.finish_pairing = await discovery.start_pairing(self.hkid)
|
||||
|
||||
except aiohomekit.BusyError:
|
||||
|
||||
@@ -3,8 +3,16 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit[IP]==0.2.49"],
|
||||
"zeroconf": ["_hap._tcp.local."],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": ["@Jc2k"]
|
||||
"requirements": [
|
||||
"aiohomekit==0.2.53"
|
||||
],
|
||||
"zeroconf": [
|
||||
"_hap._tcp.local."
|
||||
],
|
||||
"after_dependencies": [
|
||||
"zeroconf"
|
||||
],
|
||||
"codeowners": [
|
||||
"@Jc2k"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"already_configured": "Accessory is already configured with this controller.",
|
||||
"invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting configuration entry for it in Home Assistant that must first be removed.",
|
||||
"accessory_not_found_error": "Cannot add pairing as device can no longer be found.",
|
||||
"invalid_properties": "Invalid properties announced by device.",
|
||||
"already_in_progress": "Config flow for device is already in progress."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,12 @@ async def async_setup(hass, config):
|
||||
discovery.async_load_platform(hass, platform.value, DOMAIN, {}, config)
|
||||
)
|
||||
|
||||
if not hass.data[DATA_KNX].xknx.devices:
|
||||
_LOGGER.warning(
|
||||
"No KNX devices are configured. Please read "
|
||||
"https://www.home-assistant.io/blog/2020/09/17/release-115/#breaking-changes"
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_KNX_SEND,
|
||||
|
||||
@@ -29,8 +29,15 @@ PLAYABLE_MEDIA_TYPES = [
|
||||
MEDIA_TYPE_TRACK,
|
||||
]
|
||||
|
||||
CONTENT_TYPE_MEDIA_CLASS = {
|
||||
"library_music": MEDIA_CLASS_MUSIC,
|
||||
CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = {
|
||||
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
|
||||
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
|
||||
MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
|
||||
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
|
||||
MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW,
|
||||
}
|
||||
|
||||
CHILD_TYPE_MEDIA_CLASS = {
|
||||
MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
|
||||
MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
|
||||
MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
|
||||
@@ -151,8 +158,10 @@ async def build_item_response(media_library, payload):
|
||||
except UnknownMediaType:
|
||||
pass
|
||||
|
||||
return BrowseMedia(
|
||||
media_class=CONTENT_TYPE_MEDIA_CLASS[search_type],
|
||||
response = BrowseMedia(
|
||||
media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get(
|
||||
search_type, MEDIA_CLASS_DIRECTORY
|
||||
),
|
||||
media_content_id=search_id,
|
||||
media_content_type=search_type,
|
||||
title=title,
|
||||
@@ -162,6 +171,13 @@ async def build_item_response(media_library, payload):
|
||||
thumbnail=thumbnail,
|
||||
)
|
||||
|
||||
if search_type == "library_music":
|
||||
response.children_media_class = MEDIA_CLASS_MUSIC
|
||||
else:
|
||||
response.calculate_children_class()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def item_payload(item, media_library):
|
||||
"""
|
||||
@@ -170,11 +186,12 @@ def item_payload(item, media_library):
|
||||
Used by async_browse_media.
|
||||
"""
|
||||
title = item["label"]
|
||||
|
||||
thumbnail = item.get("thumbnail")
|
||||
if thumbnail:
|
||||
thumbnail = media_library.thumbnail_url(thumbnail)
|
||||
|
||||
media_class = None
|
||||
|
||||
if "songid" in item:
|
||||
media_content_type = MEDIA_TYPE_TRACK
|
||||
media_content_id = f"{item['songid']}"
|
||||
@@ -213,16 +230,18 @@ def item_payload(item, media_library):
|
||||
else:
|
||||
# this case is for the top folder of each type
|
||||
# possible content types: album, artist, movie, library_music, tvshow
|
||||
media_class = MEDIA_CLASS_DIRECTORY
|
||||
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
|
||||
if media_class is None:
|
||||
try:
|
||||
media_class = CHILD_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,
|
||||
|
||||
@@ -116,6 +116,9 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
)
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context.update({"title_placeholders": {CONF_NAME: self._name}})
|
||||
|
||||
try:
|
||||
await validate_http(self.hass, self._get_data())
|
||||
await validate_ws(self.hass, self._get_data())
|
||||
@@ -129,8 +132,6 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context.update({"title_placeholders": {CONF_NAME: self._name}})
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_discovery_confirm(self, user_input=None):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "kodi",
|
||||
"name": "Kodi",
|
||||
"documentation": "https://www.home-assistant.io/integrations/kodi",
|
||||
"requirements": ["pykodi==0.1.2"],
|
||||
"requirements": ["pykodi==0.2.0"],
|
||||
"codeowners": [
|
||||
"@OnFreund"
|
||||
],
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import re
|
||||
|
||||
import jsonrpc_base
|
||||
from pykodi import CannotConnectError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
|
||||
@@ -324,11 +325,15 @@ class KodiEntity(MediaPlayerEntity):
|
||||
self._app_properties["muted"] = data["muted"]
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def async_on_quit(self, sender, data):
|
||||
async def async_on_quit(self, sender, data):
|
||||
"""Reset the player state on quit action."""
|
||||
await self._clear_connection()
|
||||
|
||||
async def _clear_connection(self, close=True):
|
||||
self._reset_state()
|
||||
self.hass.async_create_task(self._connection.close())
|
||||
self.async_write_ha_state()
|
||||
if close:
|
||||
await self._connection.close()
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -386,14 +391,23 @@ class KodiEntity(MediaPlayerEntity):
|
||||
try:
|
||||
await self._connection.connect()
|
||||
self._on_ws_connected()
|
||||
except jsonrpc_base.jsonrpc.TransportError:
|
||||
_LOGGER.info("Unable to connect to Kodi via websocket")
|
||||
except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError):
|
||||
_LOGGER.debug("Unable to connect to Kodi via websocket", exc_info=True)
|
||||
await self._clear_connection(False)
|
||||
|
||||
async def _ping(self):
|
||||
try:
|
||||
await self._kodi.ping()
|
||||
except (jsonrpc_base.jsonrpc.TransportError, CannotConnectError):
|
||||
_LOGGER.debug("Unable to ping Kodi via websocket", exc_info=True)
|
||||
await self._clear_connection()
|
||||
|
||||
async def _async_connect_websocket_if_disconnected(self, *_):
|
||||
"""Reconnect the websocket if it fails."""
|
||||
if not self._connection.connected:
|
||||
await self._async_ws_connect()
|
||||
else:
|
||||
await self._ping()
|
||||
|
||||
@callback
|
||||
def _register_ws_callbacks(self):
|
||||
@@ -464,7 +478,7 @@ class KodiEntity(MediaPlayerEntity):
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return (not self._connection.can_subscribe) or (not self._connection.connected)
|
||||
return not self._connection.can_subscribe
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
@@ -700,7 +714,7 @@ class KodiEntity(MediaPlayerEntity):
|
||||
_LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs)
|
||||
result_ok = False
|
||||
try:
|
||||
result = self._kodi.call_method(method, **kwargs)
|
||||
result = await self._kodi.call_method(method, **kwargs)
|
||||
result_ok = True
|
||||
except jsonrpc_base.jsonrpc.ProtocolError as exc:
|
||||
result = exc.args[2]["error"]
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
"domain": "loopenergy",
|
||||
"name": "Loop Energy",
|
||||
"documentation": "https://www.home-assistant.io/integrations/loopenergy",
|
||||
"requirements": ["pyloopenergy==0.1.3"],
|
||||
"codeowners": []
|
||||
"requirements": ["pyloopenergy==0.2.1"],
|
||||
"codeowners": [
|
||||
"@pavoni"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ class LuciDeviceScanner(DeviceScanner):
|
||||
|
||||
last_results = []
|
||||
for device in result:
|
||||
last_results.append(device)
|
||||
if device.reachable:
|
||||
last_results.append(device)
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
@@ -85,6 +85,7 @@ from .const import (
|
||||
ATTR_SOUND_MODE,
|
||||
ATTR_SOUND_MODE_LIST,
|
||||
DOMAIN,
|
||||
MEDIA_CLASS_DIRECTORY,
|
||||
SERVICE_CLEAR_PLAYLIST,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOUND_MODE,
|
||||
@@ -816,24 +817,10 @@ class MediaPlayerEntity(Entity):
|
||||
media_content_type: Optional[str] = None,
|
||||
media_content_id: Optional[str] = None,
|
||||
) -> "BrowseMedia":
|
||||
"""
|
||||
Return a payload for the "media_player/browse_media" websocket command.
|
||||
"""Return a BrowseMedia instance.
|
||||
|
||||
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
|
||||
- Can be used as-is with media_player.play_media service
|
||||
"can_play": bool - If item is playable
|
||||
"can_expand": bool - If item contains other media
|
||||
"thumbnail": str (Optional) - URL to image thumbnail for item
|
||||
"children": list (Optional) - [{<item_with_keys_above>}, ...]
|
||||
}
|
||||
|
||||
Note: Children should omit the children key.
|
||||
The BrowseMedia instance will be used by the
|
||||
"media_player/browse_media" websocket command.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -1054,6 +1041,7 @@ class BrowseMedia:
|
||||
can_play: bool,
|
||||
can_expand: bool,
|
||||
children: Optional[List["BrowseMedia"]] = None,
|
||||
children_media_class: Optional[str] = None,
|
||||
thumbnail: Optional[str] = None,
|
||||
):
|
||||
"""Initialize browse media item."""
|
||||
@@ -1064,10 +1052,14 @@ class BrowseMedia:
|
||||
self.can_play = can_play
|
||||
self.can_expand = can_expand
|
||||
self.children = children
|
||||
self.children_media_class = children_media_class
|
||||
self.thumbnail = thumbnail
|
||||
|
||||
def as_dict(self, *, parent: bool = True) -> dict:
|
||||
"""Convert Media class to browse media dictionary."""
|
||||
if self.children_media_class is None:
|
||||
self.calculate_children_class()
|
||||
|
||||
response = {
|
||||
"title": self.title,
|
||||
"media_class": self.media_class,
|
||||
@@ -1075,6 +1067,7 @@ class BrowseMedia:
|
||||
"media_content_id": self.media_content_id,
|
||||
"can_play": self.can_play,
|
||||
"can_expand": self.can_expand,
|
||||
"children_media_class": self.children_media_class,
|
||||
"thumbnail": self.thumbnail,
|
||||
}
|
||||
|
||||
@@ -1089,3 +1082,14 @@ class BrowseMedia:
|
||||
response["children"] = []
|
||||
|
||||
return response
|
||||
|
||||
def calculate_children_class(self) -> None:
|
||||
"""Count the children media classes and calculate the correct class."""
|
||||
if self.children is None or len(self.children) == 0:
|
||||
return
|
||||
|
||||
self.children_media_class = MEDIA_CLASS_DIRECTORY
|
||||
|
||||
proposed_class = self.children[0].media_class
|
||||
if all(child.media_class == proposed_class for child in self.children):
|
||||
self.children_media_class = proposed_class
|
||||
|
||||
@@ -31,10 +31,8 @@ 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"
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
"""Constants for the media_source integration."""
|
||||
import re
|
||||
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_CLASS_IMAGE,
|
||||
MEDIA_CLASS_MUSIC,
|
||||
MEDIA_CLASS_VIDEO,
|
||||
)
|
||||
|
||||
DOMAIN = "media_source"
|
||||
MEDIA_MIME_TYPES = ("audio", "video", "image")
|
||||
MEDIA_CLASS_MAP = {
|
||||
"audio": MEDIA_CLASS_MUSIC,
|
||||
"video": MEDIA_CLASS_VIDEO,
|
||||
"image": MEDIA_CLASS_IMAGE,
|
||||
}
|
||||
URI_SCHEME = "media-source://"
|
||||
URI_SCHEME_REGEX = re.compile(r"^media-source://(?P<domain>[^/]+)?(?P<identifier>.+)?")
|
||||
URI_SCHEME_REGEX = re.compile(
|
||||
r"^media-source:\/\/(?:(?P<domain>(?!.+__)(?!_)[\da-z_]+(?<!_))(?:\/(?P<identifier>(?!\/).+))?)?$"
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.components.media_source.error import Unresolvable
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import sanitize_path
|
||||
|
||||
from .const import DOMAIN, MEDIA_MIME_TYPES
|
||||
from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES
|
||||
from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia
|
||||
|
||||
|
||||
@@ -21,26 +21,7 @@ def async_setup(hass: HomeAssistant):
|
||||
"""Set up local media source."""
|
||||
source = LocalSource(hass)
|
||||
hass.data[DOMAIN][DOMAIN] = source
|
||||
hass.http.register_view(LocalMediaView(hass))
|
||||
|
||||
|
||||
@callback
|
||||
def async_parse_identifier(item: MediaSourceItem) -> Tuple[str, str]:
|
||||
"""Parse identifier."""
|
||||
if not item.identifier:
|
||||
source_dir_id = "media"
|
||||
location = ""
|
||||
|
||||
else:
|
||||
source_dir_id, location = item.identifier.lstrip("/").split("/", 1)
|
||||
|
||||
if source_dir_id != "media":
|
||||
raise Unresolvable("Unknown source directory.")
|
||||
|
||||
if location != sanitize_path(location):
|
||||
raise Unresolvable("Invalid path.")
|
||||
|
||||
return source_dir_id, location
|
||||
hass.http.register_view(LocalMediaView(hass, source))
|
||||
|
||||
|
||||
class LocalSource(MediaSource):
|
||||
@@ -56,22 +37,41 @@ class LocalSource(MediaSource):
|
||||
@callback
|
||||
def async_full_path(self, source_dir_id, location) -> Path:
|
||||
"""Return full path."""
|
||||
return self.hass.config.path("media", location)
|
||||
return Path(self.hass.config.media_dirs[source_dir_id], location)
|
||||
|
||||
@callback
|
||||
def async_parse_identifier(self, item: MediaSourceItem) -> Tuple[str, str]:
|
||||
"""Parse identifier."""
|
||||
if not item.identifier:
|
||||
# Empty source_dir_id and location
|
||||
return "", ""
|
||||
|
||||
source_dir_id, location = item.identifier.split("/", 1)
|
||||
if source_dir_id not in self.hass.config.media_dirs:
|
||||
raise Unresolvable("Unknown source directory.")
|
||||
|
||||
if location != sanitize_path(location):
|
||||
raise Unresolvable("Invalid path.")
|
||||
|
||||
return source_dir_id, location
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> str:
|
||||
"""Resolve media to a url."""
|
||||
source_dir_id, location = async_parse_identifier(item)
|
||||
source_dir_id, location = self.async_parse_identifier(item)
|
||||
if source_dir_id == "" or source_dir_id not in self.hass.config.media_dirs:
|
||||
raise Unresolvable("Unknown source directory.")
|
||||
|
||||
mime_type, _ = mimetypes.guess_type(
|
||||
self.async_full_path(source_dir_id, location)
|
||||
str(self.async_full_path(source_dir_id, location))
|
||||
)
|
||||
return PlayMedia(item.identifier, mime_type)
|
||||
return PlayMedia(f"/media/{item.identifier}", mime_type)
|
||||
|
||||
async def async_browse_media(
|
||||
self, item: MediaSourceItem, media_types: Tuple[str] = MEDIA_MIME_TYPES
|
||||
) -> BrowseMediaSource:
|
||||
"""Return media."""
|
||||
try:
|
||||
source_dir_id, location = async_parse_identifier(item)
|
||||
source_dir_id, location = self.async_parse_identifier(item)
|
||||
except Unresolvable as err:
|
||||
raise BrowseError(str(err)) from err
|
||||
|
||||
@@ -79,9 +79,37 @@ class LocalSource(MediaSource):
|
||||
self._browse_media, source_dir_id, location
|
||||
)
|
||||
|
||||
def _browse_media(self, source_dir_id, location):
|
||||
def _browse_media(self, source_dir_id: str, location: Path):
|
||||
"""Browse media."""
|
||||
full_path = Path(self.hass.config.path("media", location))
|
||||
|
||||
# If only one media dir is configured, use that as the local media root
|
||||
if source_dir_id == "" and len(self.hass.config.media_dirs) == 1:
|
||||
source_dir_id = list(self.hass.config.media_dirs)[0]
|
||||
|
||||
# Multiple folder, root is requested
|
||||
if source_dir_id == "":
|
||||
if location:
|
||||
raise BrowseError("Folder not found.")
|
||||
|
||||
base = BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier="",
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_type=None,
|
||||
title=self.name,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children_media_class=MEDIA_CLASS_DIRECTORY,
|
||||
)
|
||||
|
||||
base.children = [
|
||||
self._browse_media(source_dir_id, "")
|
||||
for source_dir_id in self.hass.config.media_dirs
|
||||
]
|
||||
|
||||
return base
|
||||
|
||||
full_path = Path(self.hass.config.media_dirs[source_dir_id], location)
|
||||
|
||||
if not full_path.exists():
|
||||
if location == "":
|
||||
@@ -112,11 +140,15 @@ class LocalSource(MediaSource):
|
||||
if is_dir:
|
||||
title += "/"
|
||||
|
||||
media_class = MEDIA_CLASS_MAP.get(
|
||||
mime_type and mime_type.split("/")[0], MEDIA_CLASS_DIRECTORY
|
||||
)
|
||||
|
||||
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",
|
||||
identifier=f"{source_dir_id}/{path.relative_to(self.hass.config.media_dirs[source_dir_id])}",
|
||||
media_class=media_class,
|
||||
media_content_type=mime_type or "",
|
||||
title=title,
|
||||
can_play=is_file,
|
||||
can_expand=is_dir,
|
||||
@@ -132,6 +164,9 @@ class LocalSource(MediaSource):
|
||||
if child:
|
||||
media.children.append(child)
|
||||
|
||||
# Sort children showing directories first, then by name
|
||||
media.children.sort(key=lambda child: (child.can_play, child.title))
|
||||
|
||||
return media
|
||||
|
||||
|
||||
@@ -142,19 +177,25 @@ class LocalMediaView(HomeAssistantView):
|
||||
Returns media files in config/media.
|
||||
"""
|
||||
|
||||
url = "/media/{location:.*}"
|
||||
url = "/media/{source_dir_id}/{location:.*}"
|
||||
name = "media"
|
||||
|
||||
def __init__(self, hass: HomeAssistant):
|
||||
def __init__(self, hass: HomeAssistant, source: LocalSource):
|
||||
"""Initialize the media view."""
|
||||
self.hass = hass
|
||||
self.source = source
|
||||
|
||||
async def get(self, request: web.Request, location: str) -> web.FileResponse:
|
||||
async def get(
|
||||
self, request: web.Request, source_dir_id: str, location: str
|
||||
) -> web.FileResponse:
|
||||
"""Start a GET request."""
|
||||
if location != sanitize_path(location):
|
||||
return web.HTTPNotFound()
|
||||
raise web.HTTPNotFound()
|
||||
|
||||
media_path = Path(self.hass.config.path("media", location))
|
||||
if source_dir_id not in self.hass.config.media_dirs:
|
||||
raise web.HTTPNotFound()
|
||||
|
||||
media_path = self.source.async_full_path(source_dir_id, location)
|
||||
|
||||
# Check that the file exists
|
||||
if not media_path.is_file():
|
||||
|
||||
@@ -6,7 +6,7 @@ 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_CLASS_DIRECTORY,
|
||||
MEDIA_TYPE_CHANNEL,
|
||||
MEDIA_TYPE_CHANNELS,
|
||||
)
|
||||
@@ -54,11 +54,12 @@ class MediaSourceItem:
|
||||
base = BrowseMediaSource(
|
||||
domain=None,
|
||||
identifier=None,
|
||||
media_class=MEDIA_CLASS_CHANNELS,
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_type=MEDIA_TYPE_CHANNELS,
|
||||
title="Media Sources",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children_media_class=MEDIA_CLASS_CHANNEL,
|
||||
)
|
||||
base.children = [
|
||||
BrowseMediaSource(
|
||||
|
||||
@@ -104,7 +104,6 @@ class MetWeather(CoordinatorEntity, WeatherEntity):
|
||||
self._config = config
|
||||
self._is_metric = is_metric
|
||||
self._hourly = hourly
|
||||
self._name_appendix = "-hourly" if hourly else ""
|
||||
|
||||
@property
|
||||
def track_home(self):
|
||||
@@ -114,23 +113,34 @@ class MetWeather(CoordinatorEntity, WeatherEntity):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID."""
|
||||
name_appendix = ""
|
||||
if self._hourly:
|
||||
name_appendix = "-hourly"
|
||||
if self.track_home:
|
||||
return f"home{self._name_appendix}"
|
||||
return f"home{name_appendix}"
|
||||
|
||||
return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{self._name_appendix}"
|
||||
return f"{self._config[CONF_LATITUDE]}-{self._config[CONF_LONGITUDE]}{name_appendix}"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
name = self._config.get(CONF_NAME)
|
||||
name_appendix = ""
|
||||
if self._hourly:
|
||||
name_appendix = " Hourly"
|
||||
|
||||
if name is not None:
|
||||
return f"{name}{self._name_appendix}"
|
||||
return f"{name}{name_appendix}"
|
||||
|
||||
if self.track_home:
|
||||
return f"{self.hass.config.location_name}{self._name_appendix}"
|
||||
return f"{self.hass.config.location_name}{name_appendix}"
|
||||
|
||||
return f"{DEFAULT_NAME}{self._name_appendix}"
|
||||
return f"{DEFAULT_NAME}{name_appendix}"
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return not self._hourly
|
||||
|
||||
@property
|
||||
def condition(self):
|
||||
|
||||
@@ -79,9 +79,10 @@ class MeteoFranceSensor(CoordinatorEntity):
|
||||
"""Initialize the Meteo-France sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._type = sensor_type
|
||||
city_name = self.coordinator.data.position["name"]
|
||||
self._name = f"{city_name} {SENSOR_TYPES[self._type][ENTITY_NAME]}"
|
||||
self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}_{self._type}"
|
||||
if hasattr(self.coordinator.data, "position"):
|
||||
city_name = self.coordinator.data.position["name"]
|
||||
self._name = f"{city_name} {SENSOR_TYPES[self._type][ENTITY_NAME]}"
|
||||
self._unique_id = f"{self.coordinator.data.position['lat']},{self.coordinator.data.position['lon']}_{self._type}"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
||||
@@ -171,7 +171,7 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity):
|
||||
return
|
||||
|
||||
self._available = True
|
||||
return bool(result.bits[0])
|
||||
return bool(result.bits[coil])
|
||||
|
||||
def _write_coil(self, coil, value):
|
||||
"""Write coil using the Modbus hub slave."""
|
||||
|
||||
@@ -138,12 +138,12 @@ class MpdDevice(MediaPlayerEntity):
|
||||
if position is None:
|
||||
position = self._status.get("time")
|
||||
|
||||
if position is not None and ":" in position:
|
||||
if isinstance(position, str) and ":" in position:
|
||||
position = position.split(":")[0]
|
||||
|
||||
if position is not None and self._media_position != position:
|
||||
self._media_position_updated_at = dt_util.utcnow()
|
||||
self._media_position = int(position)
|
||||
self._media_position = int(float(position))
|
||||
|
||||
self._update_playlists()
|
||||
|
||||
@@ -159,8 +159,9 @@ class MpdDevice(MediaPlayerEntity):
|
||||
self._connect()
|
||||
|
||||
self._fetch_status()
|
||||
except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError):
|
||||
except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError) as error:
|
||||
# Cleanly disconnect in case connection is not in valid state
|
||||
_LOGGER.debug("Error updating status: %s", error)
|
||||
self._disconnect()
|
||||
|
||||
@property
|
||||
|
||||
@@ -60,6 +60,7 @@ from .const import (
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_WILL_MESSAGE,
|
||||
DATA_MQTT_CONFIG,
|
||||
DEFAULT_BIRTH,
|
||||
DEFAULT_DISCOVERY,
|
||||
DEFAULT_PAYLOAD_AVAILABLE,
|
||||
@@ -88,7 +89,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = "mqtt"
|
||||
|
||||
DATA_MQTT = "mqtt"
|
||||
DATA_MQTT_CONFIG = "mqtt_config"
|
||||
|
||||
SERVICE_PUBLISH = "publish"
|
||||
SERVICE_DUMP = "dump"
|
||||
@@ -134,7 +134,7 @@ CONNECTION_FAILED = "connection_failed"
|
||||
CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable"
|
||||
|
||||
DISCOVERY_COOLDOWN = 2
|
||||
TIMEOUT_ACK = 1
|
||||
TIMEOUT_ACK = 10
|
||||
|
||||
PLATFORMS = [
|
||||
"alarm_control_panel",
|
||||
@@ -1305,7 +1305,7 @@ class MqttDiscoveryUpdate(Entity):
|
||||
debug_info.add_entity_discovery_data(
|
||||
self.hass, self._discovery_data, self.entity_id
|
||||
)
|
||||
# Set in case the entity has been removed and is re-added
|
||||
# Set in case the entity has been removed and is re-added, for example when changing entity_id
|
||||
set_discovery_hash(self.hass, discovery_hash)
|
||||
self._remove_signal = async_dispatcher_connect(
|
||||
self.hass,
|
||||
|
||||
@@ -104,7 +104,7 @@ async def async_setup_platform(
|
||||
):
|
||||
"""Set up MQTT alarm control panel through configuration.yaml."""
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
await _async_setup_entity(config, async_add_entities)
|
||||
await _async_setup_entity(hass, config, async_add_entities)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@@ -116,7 +116,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
try:
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
await _async_setup_entity(
|
||||
config, async_add_entities, config_entry, discovery_data
|
||||
hass, config, async_add_entities, config_entry, discovery_data
|
||||
)
|
||||
except Exception:
|
||||
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
|
||||
@@ -128,10 +128,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
|
||||
|
||||
async def _async_setup_entity(
|
||||
config, async_add_entities, config_entry=None, discovery_data=None
|
||||
hass, config, async_add_entities, config_entry=None, discovery_data=None
|
||||
):
|
||||
"""Set up the MQTT Alarm Control Panel platform."""
|
||||
async_add_entities([MqttAlarm(config, config_entry, discovery_data)])
|
||||
async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)])
|
||||
|
||||
|
||||
class MqttAlarm(
|
||||
@@ -143,13 +143,16 @@ class MqttAlarm(
|
||||
):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
def __init__(self, config, config_entry, discovery_data):
|
||||
def __init__(self, hass, config, config_entry, discovery_data):
|
||||
"""Init the MQTT Alarm Control Panel."""
|
||||
self.hass = hass
|
||||
self._state = None
|
||||
self._config = config
|
||||
self._unique_id = config.get(CONF_UNIQUE_ID)
|
||||
self._sub_state = None
|
||||
|
||||
# Load config
|
||||
self._setup_from_config(config)
|
||||
|
||||
device_config = config.get(CONF_DEVICE)
|
||||
|
||||
MqttAttributes.__init__(self, config)
|
||||
@@ -165,26 +168,30 @@ class MqttAlarm(
|
||||
async def discovery_update(self, discovery_payload):
|
||||
"""Handle updated discovery message."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
self._config = config
|
||||
self._setup_from_config(config)
|
||||
await self.attributes_discovery_update(config)
|
||||
await self.availability_discovery_update(config)
|
||||
await self.device_info_discovery_update(config)
|
||||
await self._subscribe_topics()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
def _setup_from_config(self, config):
|
||||
self._config = config
|
||||
value_template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = self.hass
|
||||
command_template = self._config[CONF_COMMAND_TEMPLATE]
|
||||
command_template.hass = self.hass
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def message_received(msg):
|
||||
"""Run when new MQTT message has been received."""
|
||||
payload = msg.payload
|
||||
value_template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
msg.payload, self._state
|
||||
|
||||
@@ -76,7 +76,7 @@ async def async_setup_platform(
|
||||
):
|
||||
"""Set up MQTT binary sensor through configuration.yaml."""
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
await _async_setup_entity(config, async_add_entities)
|
||||
await _async_setup_entity(hass, config, async_add_entities)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@@ -88,7 +88,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
try:
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
await _async_setup_entity(
|
||||
config, async_add_entities, config_entry, discovery_data
|
||||
hass, config, async_add_entities, config_entry, discovery_data
|
||||
)
|
||||
except Exception:
|
||||
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
|
||||
@@ -100,10 +100,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
|
||||
|
||||
async def _async_setup_entity(
|
||||
config, async_add_entities, config_entry=None, discovery_data=None
|
||||
hass, config, async_add_entities, config_entry=None, discovery_data=None
|
||||
):
|
||||
"""Set up the MQTT binary sensor."""
|
||||
async_add_entities([MqttBinarySensor(config, config_entry, discovery_data)])
|
||||
async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)])
|
||||
|
||||
|
||||
class MqttBinarySensor(
|
||||
@@ -115,9 +115,9 @@ class MqttBinarySensor(
|
||||
):
|
||||
"""Representation a binary sensor that is updated by MQTT."""
|
||||
|
||||
def __init__(self, config, config_entry, discovery_data):
|
||||
def __init__(self, hass, config, config_entry, discovery_data):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
self._config = config
|
||||
self.hass = hass
|
||||
self._unique_id = config.get(CONF_UNIQUE_ID)
|
||||
self._state = None
|
||||
self._sub_state = None
|
||||
@@ -128,6 +128,10 @@ class MqttBinarySensor(
|
||||
self._expired = True
|
||||
else:
|
||||
self._expired = None
|
||||
|
||||
# Load config
|
||||
self._setup_from_config(config)
|
||||
|
||||
device_config = config.get(CONF_DEVICE)
|
||||
|
||||
MqttAttributes.__init__(self, config)
|
||||
@@ -143,19 +147,22 @@ class MqttBinarySensor(
|
||||
async def discovery_update(self, discovery_payload):
|
||||
"""Handle updated discovery message."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
self._config = config
|
||||
self._setup_from_config(config)
|
||||
await self.attributes_discovery_update(config)
|
||||
await self.availability_discovery_update(config)
|
||||
await self.device_info_discovery_update(config)
|
||||
await self._subscribe_topics()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
def _setup_from_config(self, config):
|
||||
self._config = config
|
||||
value_template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = self.hass
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
@callback
|
||||
def off_delay_listener(now):
|
||||
"""Switch device off after a delay."""
|
||||
|
||||
@@ -24,6 +24,7 @@ from .const import (
|
||||
CONF_BROKER,
|
||||
CONF_DISCOVERY,
|
||||
CONF_WILL_MESSAGE,
|
||||
DATA_MQTT_CONFIG,
|
||||
DEFAULT_BIRTH,
|
||||
DEFAULT_DISCOVERY,
|
||||
DEFAULT_WILL,
|
||||
@@ -162,6 +163,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Manage the MQTT options."""
|
||||
errors = {}
|
||||
current_config = self.config_entry.data
|
||||
yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {})
|
||||
if user_input is not None:
|
||||
can_connect = await self.hass.async_add_executor_job(
|
||||
try_connection,
|
||||
@@ -178,20 +180,22 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
fields = OrderedDict()
|
||||
fields[vol.Required(CONF_BROKER, default=current_config[CONF_BROKER])] = str
|
||||
fields[vol.Required(CONF_PORT, default=current_config[CONF_PORT])] = vol.Coerce(
|
||||
int
|
||||
)
|
||||
current_broker = current_config.get(CONF_BROKER, yaml_config.get(CONF_BROKER))
|
||||
current_port = current_config.get(CONF_PORT, yaml_config.get(CONF_PORT))
|
||||
current_user = current_config.get(CONF_USERNAME, yaml_config.get(CONF_USERNAME))
|
||||
current_pass = current_config.get(CONF_PASSWORD, yaml_config.get(CONF_PASSWORD))
|
||||
fields[vol.Required(CONF_BROKER, default=current_broker)] = str
|
||||
fields[vol.Required(CONF_PORT, default=current_port)] = vol.Coerce(int)
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_USERNAME,
|
||||
description={"suggested_value": current_config.get(CONF_USERNAME)},
|
||||
description={"suggested_value": current_user},
|
||||
)
|
||||
] = str
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_PASSWORD,
|
||||
description={"suggested_value": current_config.get(CONF_PASSWORD)},
|
||||
description={"suggested_value": current_pass},
|
||||
)
|
||||
] = str
|
||||
|
||||
@@ -205,6 +209,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Manage the MQTT options."""
|
||||
errors = {}
|
||||
current_config = self.config_entry.data
|
||||
yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {})
|
||||
options_config = {}
|
||||
if user_input is not None:
|
||||
bad_birth = False
|
||||
@@ -253,16 +258,24 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
)
|
||||
return self.async_create_entry(title="", data=None)
|
||||
|
||||
birth = {**DEFAULT_BIRTH, **current_config.get(CONF_BIRTH_MESSAGE, {})}
|
||||
will = {**DEFAULT_WILL, **current_config.get(CONF_WILL_MESSAGE, {})}
|
||||
birth = {
|
||||
**DEFAULT_BIRTH,
|
||||
**current_config.get(
|
||||
CONF_BIRTH_MESSAGE, yaml_config.get(CONF_BIRTH_MESSAGE, {})
|
||||
),
|
||||
}
|
||||
will = {
|
||||
**DEFAULT_WILL,
|
||||
**current_config.get(
|
||||
CONF_WILL_MESSAGE, yaml_config.get(CONF_WILL_MESSAGE, {})
|
||||
),
|
||||
}
|
||||
discovery = current_config.get(
|
||||
CONF_DISCOVERY, yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY)
|
||||
)
|
||||
|
||||
fields = OrderedDict()
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_DISCOVERY,
|
||||
default=current_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY),
|
||||
)
|
||||
] = bool
|
||||
fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = bool
|
||||
|
||||
# Birth message is disabled if CONF_BIRTH_MESSAGE = {}
|
||||
fields[
|
||||
|
||||
@@ -17,6 +17,8 @@ CONF_RETAIN = ATTR_RETAIN
|
||||
CONF_STATE_TOPIC = "state_topic"
|
||||
CONF_WILL_MESSAGE = "will_message"
|
||||
|
||||
DATA_MQTT_CONFIG = "mqtt_config"
|
||||
|
||||
DEFAULT_PREFIX = "homeassistant"
|
||||
DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status"
|
||||
DEFAULT_DISCOVERY = False
|
||||
|
||||
@@ -174,7 +174,7 @@ async def async_setup_platform(
|
||||
):
|
||||
"""Set up MQTT cover through configuration.yaml."""
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
await _async_setup_entity(config, async_add_entities)
|
||||
await _async_setup_entity(hass, config, async_add_entities)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@@ -186,7 +186,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
try:
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
await _async_setup_entity(
|
||||
config, async_add_entities, config_entry, discovery_data
|
||||
hass, config, async_add_entities, config_entry, discovery_data
|
||||
)
|
||||
except Exception:
|
||||
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
|
||||
@@ -198,10 +198,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
|
||||
|
||||
async def _async_setup_entity(
|
||||
config, async_add_entities, config_entry=None, discovery_data=None
|
||||
hass, config, async_add_entities, config_entry=None, discovery_data=None
|
||||
):
|
||||
"""Set up the MQTT Cover."""
|
||||
async_add_entities([MqttCover(config, config_entry, discovery_data)])
|
||||
async_add_entities([MqttCover(hass, config, config_entry, discovery_data)])
|
||||
|
||||
|
||||
class MqttCover(
|
||||
@@ -213,8 +213,9 @@ class MqttCover(
|
||||
):
|
||||
"""Representation of a cover that can be controlled using MQTT."""
|
||||
|
||||
def __init__(self, config, config_entry, discovery_data):
|
||||
def __init__(self, hass, config, config_entry, discovery_data):
|
||||
"""Initialize the cover."""
|
||||
self.hass = hass
|
||||
self._unique_id = config.get(CONF_UNIQUE_ID)
|
||||
self._position = None
|
||||
self._state = None
|
||||
@@ -257,8 +258,6 @@ class MqttCover(
|
||||
)
|
||||
self._tilt_optimistic = config[CONF_TILT_STATE_OPTIMISTIC]
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if template is not None:
|
||||
template.hass = self.hass
|
||||
@@ -269,6 +268,8 @@ class MqttCover(
|
||||
if tilt_status_template is not None:
|
||||
tilt_status_template.hass = self.hass
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics = {}
|
||||
|
||||
@callback
|
||||
@@ -276,6 +277,7 @@ class MqttCover(
|
||||
def tilt_message_received(msg):
|
||||
"""Handle tilt updates."""
|
||||
payload = msg.payload
|
||||
tilt_status_template = self._config.get(CONF_TILT_STATUS_TEMPLATE)
|
||||
if tilt_status_template is not None:
|
||||
payload = tilt_status_template.async_render_with_possible_json_value(
|
||||
payload
|
||||
@@ -296,6 +298,7 @@ class MqttCover(
|
||||
def state_message_received(msg):
|
||||
"""Handle new MQTT state messages."""
|
||||
payload = msg.payload
|
||||
template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if template is not None:
|
||||
payload = template.async_render_with_possible_json_value(payload)
|
||||
|
||||
@@ -321,6 +324,7 @@ class MqttCover(
|
||||
def position_message_received(msg):
|
||||
"""Handle new MQTT state messages."""
|
||||
payload = msg.payload
|
||||
template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if template is not None:
|
||||
payload = template.async_render_with_possible_json_value(payload)
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ async def async_setup_platform(
|
||||
):
|
||||
"""Set up MQTT fan through configuration.yaml."""
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
await _async_setup_entity(config, async_add_entities)
|
||||
await _async_setup_entity(hass, config, async_add_entities)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@@ -127,7 +127,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
try:
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
await _async_setup_entity(
|
||||
config, async_add_entities, config_entry, discovery_data
|
||||
hass, config, async_add_entities, config_entry, discovery_data
|
||||
)
|
||||
except Exception:
|
||||
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
|
||||
@@ -139,10 +139,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
|
||||
|
||||
async def _async_setup_entity(
|
||||
config, async_add_entities, config_entry=None, discovery_data=None
|
||||
hass, config, async_add_entities, config_entry=None, discovery_data=None
|
||||
):
|
||||
"""Set up the MQTT fan."""
|
||||
async_add_entities([MqttFan(config, config_entry, discovery_data)])
|
||||
async_add_entities([MqttFan(hass, config, config_entry, discovery_data)])
|
||||
|
||||
|
||||
class MqttFan(
|
||||
@@ -154,8 +154,9 @@ class MqttFan(
|
||||
):
|
||||
"""A MQTT fan component."""
|
||||
|
||||
def __init__(self, config, config_entry, discovery_data):
|
||||
def __init__(self, hass, config, config_entry, discovery_data):
|
||||
"""Initialize the MQTT fan."""
|
||||
self.hass = hass
|
||||
self._unique_id = config.get(CONF_UNIQUE_ID)
|
||||
self._state = False
|
||||
self._speed = None
|
||||
@@ -242,22 +243,22 @@ class MqttFan(
|
||||
self._topic[CONF_SPEED_COMMAND_TOPIC] is not None and SUPPORT_SET_SPEED
|
||||
)
|
||||
|
||||
for key, tpl in list(self._templates.items()):
|
||||
if tpl is None:
|
||||
self._templates[key] = lambda value: value
|
||||
else:
|
||||
tpl.hass = self.hass
|
||||
self._templates[key] = tpl.async_render_with_possible_json_value
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics = {}
|
||||
templates = {}
|
||||
for key, tpl in list(self._templates.items()):
|
||||
if tpl is None:
|
||||
templates[key] = lambda value: value
|
||||
else:
|
||||
tpl.hass = self.hass
|
||||
templates[key] = tpl.async_render_with_possible_json_value
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def state_received(msg):
|
||||
"""Handle new received MQTT message."""
|
||||
payload = templates[CONF_STATE](msg.payload)
|
||||
payload = self._templates[CONF_STATE](msg.payload)
|
||||
if payload == self._payload["STATE_ON"]:
|
||||
self._state = True
|
||||
elif payload == self._payload["STATE_OFF"]:
|
||||
@@ -275,7 +276,7 @@ class MqttFan(
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def speed_received(msg):
|
||||
"""Handle new received MQTT message for the speed."""
|
||||
payload = templates[ATTR_SPEED](msg.payload)
|
||||
payload = self._templates[ATTR_SPEED](msg.payload)
|
||||
if payload == self._payload["SPEED_LOW"]:
|
||||
self._speed = SPEED_LOW
|
||||
elif payload == self._payload["SPEED_MEDIUM"]:
|
||||
@@ -298,7 +299,7 @@ class MqttFan(
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def oscillation_received(msg):
|
||||
"""Handle new received MQTT message for the oscillation."""
|
||||
payload = templates[OSCILLATION](msg.payload)
|
||||
payload = self._templates[OSCILLATION](msg.payload)
|
||||
if payload == self._payload["OSCILLATE_ON_PAYLOAD"]:
|
||||
self._oscillation = True
|
||||
elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]:
|
||||
|
||||
@@ -254,7 +254,7 @@ class MqttLight(
|
||||
|
||||
value_templates = {}
|
||||
for key in VALUE_TEMPLATE_KEYS:
|
||||
value_templates[key] = lambda value: value
|
||||
value_templates[key] = lambda value, _: value
|
||||
for key in VALUE_TEMPLATE_KEYS & config.keys():
|
||||
tpl = config[key]
|
||||
value_templates[key] = tpl.async_render_with_possible_json_value
|
||||
@@ -304,7 +304,9 @@ class MqttLight(
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def state_received(msg):
|
||||
"""Handle new MQTT messages."""
|
||||
payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE](msg.payload)
|
||||
payload = self._value_templates[CONF_STATE_VALUE_TEMPLATE](
|
||||
msg.payload, None
|
||||
)
|
||||
if not payload:
|
||||
_LOGGER.debug("Ignoring empty state message from '%s'", msg.topic)
|
||||
return
|
||||
@@ -328,7 +330,9 @@ class MqttLight(
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def brightness_received(msg):
|
||||
"""Handle new MQTT messages for the brightness."""
|
||||
payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE](msg.payload)
|
||||
payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE](
|
||||
msg.payload, None
|
||||
)
|
||||
if not payload:
|
||||
_LOGGER.debug("Ignoring empty brightness message from '%s'", msg.topic)
|
||||
return
|
||||
@@ -360,7 +364,7 @@ class MqttLight(
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def rgb_received(msg):
|
||||
"""Handle new MQTT messages for RGB."""
|
||||
payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload)
|
||||
payload = self._value_templates[CONF_RGB_VALUE_TEMPLATE](msg.payload, None)
|
||||
if not payload:
|
||||
_LOGGER.debug("Ignoring empty rgb message from '%s'", msg.topic)
|
||||
return
|
||||
@@ -392,7 +396,9 @@ class MqttLight(
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def color_temp_received(msg):
|
||||
"""Handle new MQTT messages for color temperature."""
|
||||
payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE](msg.payload)
|
||||
payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE](
|
||||
msg.payload, None
|
||||
)
|
||||
if not payload:
|
||||
_LOGGER.debug("Ignoring empty color temp message from '%s'", msg.topic)
|
||||
return
|
||||
@@ -422,7 +428,9 @@ class MqttLight(
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def effect_received(msg):
|
||||
"""Handle new MQTT messages for effect."""
|
||||
payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE](msg.payload)
|
||||
payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE](
|
||||
msg.payload, None
|
||||
)
|
||||
if not payload:
|
||||
_LOGGER.debug("Ignoring empty effect message from '%s'", msg.topic)
|
||||
return
|
||||
@@ -452,7 +460,7 @@ class MqttLight(
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def hs_received(msg):
|
||||
"""Handle new MQTT messages for hs color."""
|
||||
payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload)
|
||||
payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload, None)
|
||||
if not payload:
|
||||
_LOGGER.debug("Ignoring empty hs message from '%s'", msg.topic)
|
||||
return
|
||||
@@ -484,7 +492,9 @@ class MqttLight(
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def white_value_received(msg):
|
||||
"""Handle new MQTT messages for white value."""
|
||||
payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE](msg.payload)
|
||||
payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE](
|
||||
msg.payload, None
|
||||
)
|
||||
if not payload:
|
||||
_LOGGER.debug("Ignoring empty white value message from '%s'", msg.topic)
|
||||
return
|
||||
@@ -516,7 +526,7 @@ class MqttLight(
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def xy_received(msg):
|
||||
"""Handle new MQTT messages for xy color."""
|
||||
payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload)
|
||||
payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload, None)
|
||||
if not payload:
|
||||
_LOGGER.debug("Ignoring empty xy-color message from '%s'", msg.topic)
|
||||
return
|
||||
|
||||
@@ -77,7 +77,7 @@ async def async_setup_platform(
|
||||
):
|
||||
"""Set up MQTT lock panel through configuration.yaml."""
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
await _async_setup_entity(config, async_add_entities)
|
||||
await _async_setup_entity(hass, config, async_add_entities)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@@ -89,7 +89,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
try:
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
await _async_setup_entity(
|
||||
config, async_add_entities, config_entry, discovery_data
|
||||
hass, config, async_add_entities, config_entry, discovery_data
|
||||
)
|
||||
except Exception:
|
||||
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
|
||||
@@ -101,10 +101,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
|
||||
|
||||
async def _async_setup_entity(
|
||||
config, async_add_entities, config_entry=None, discovery_data=None
|
||||
hass, config, async_add_entities, config_entry=None, discovery_data=None
|
||||
):
|
||||
"""Set up the MQTT Lock platform."""
|
||||
async_add_entities([MqttLock(config, config_entry, discovery_data)])
|
||||
async_add_entities([MqttLock(hass, config, config_entry, discovery_data)])
|
||||
|
||||
|
||||
class MqttLock(
|
||||
@@ -116,8 +116,9 @@ class MqttLock(
|
||||
):
|
||||
"""Representation of a lock that can be toggled using MQTT."""
|
||||
|
||||
def __init__(self, config, config_entry, discovery_data):
|
||||
def __init__(self, hass, config, config_entry, discovery_data):
|
||||
"""Initialize the lock."""
|
||||
self.hass = hass
|
||||
self._unique_id = config.get(CONF_UNIQUE_ID)
|
||||
self._state = False
|
||||
self._sub_state = None
|
||||
@@ -154,17 +155,19 @@ class MqttLock(
|
||||
|
||||
self._optimistic = config[CONF_OPTIMISTIC]
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
value_template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = self.hass
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def message_received(msg):
|
||||
"""Handle new MQTT messages."""
|
||||
payload = msg.payload
|
||||
value_template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
payload = value_template.async_render_with_possible_json_value(payload)
|
||||
if payload == self._config[CONF_STATE_LOCKED]:
|
||||
|
||||
@@ -70,7 +70,7 @@ async def async_setup_platform(
|
||||
):
|
||||
"""Set up MQTT sensors through configuration.yaml."""
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
await _async_setup_entity(config, async_add_entities)
|
||||
await _async_setup_entity(hass, config, async_add_entities)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@@ -82,7 +82,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
try:
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
await _async_setup_entity(
|
||||
config, async_add_entities, config_entry, discovery_data
|
||||
hass, config, async_add_entities, config_entry, discovery_data
|
||||
)
|
||||
except Exception:
|
||||
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
|
||||
@@ -94,10 +94,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
|
||||
|
||||
async def _async_setup_entity(
|
||||
config: ConfigType, async_add_entities, config_entry=None, discovery_data=None
|
||||
hass, config: ConfigType, async_add_entities, config_entry=None, discovery_data=None
|
||||
):
|
||||
"""Set up MQTT sensor."""
|
||||
async_add_entities([MqttSensor(config, config_entry, discovery_data)])
|
||||
async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)])
|
||||
|
||||
|
||||
class MqttSensor(
|
||||
@@ -105,9 +105,9 @@ class MqttSensor(
|
||||
):
|
||||
"""Representation of a sensor that can be updated using MQTT."""
|
||||
|
||||
def __init__(self, config, config_entry, discovery_data):
|
||||
def __init__(self, hass, config, config_entry, discovery_data):
|
||||
"""Initialize the sensor."""
|
||||
self._config = config
|
||||
self.hass = hass
|
||||
self._unique_id = config.get(CONF_UNIQUE_ID)
|
||||
self._state = None
|
||||
self._sub_state = None
|
||||
@@ -118,6 +118,10 @@ class MqttSensor(
|
||||
self._expired = True
|
||||
else:
|
||||
self._expired = None
|
||||
|
||||
# Load config
|
||||
self._setup_from_config(config)
|
||||
|
||||
device_config = config.get(CONF_DEVICE)
|
||||
|
||||
MqttAttributes.__init__(self, config)
|
||||
@@ -133,19 +137,23 @@ class MqttSensor(
|
||||
async def discovery_update(self, discovery_payload):
|
||||
"""Handle updated discovery message."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
self._config = config
|
||||
self._setup_from_config(config)
|
||||
await self.attributes_discovery_update(config)
|
||||
await self.availability_discovery_update(config)
|
||||
await self.device_info_discovery_update(config)
|
||||
await self._subscribe_topics()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
def _setup_from_config(self, config):
|
||||
"""(Re)Setup the entity."""
|
||||
self._config = config
|
||||
template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if template is not None:
|
||||
template.hass = self.hass
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def message_received(msg):
|
||||
@@ -169,6 +177,7 @@ class MqttSensor(
|
||||
self.hass, self._value_is_expired, expiration_at
|
||||
)
|
||||
|
||||
template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if template is not None:
|
||||
payload = template.async_render_with_possible_json_value(
|
||||
payload, self._state
|
||||
|
||||
@@ -73,7 +73,7 @@ async def async_setup_platform(
|
||||
):
|
||||
"""Set up MQTT switch through configuration.yaml."""
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
await _async_setup_entity(config, async_add_entities, discovery_info)
|
||||
await _async_setup_entity(hass, config, async_add_entities, discovery_info)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
@@ -85,7 +85,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
try:
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
await _async_setup_entity(
|
||||
config, async_add_entities, config_entry, discovery_data
|
||||
hass, config, async_add_entities, config_entry, discovery_data
|
||||
)
|
||||
except Exception:
|
||||
clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH])
|
||||
@@ -97,10 +97,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
|
||||
|
||||
async def _async_setup_entity(
|
||||
config, async_add_entities, config_entry=None, discovery_data=None
|
||||
hass, config, async_add_entities, config_entry=None, discovery_data=None
|
||||
):
|
||||
"""Set up the MQTT switch."""
|
||||
async_add_entities([MqttSwitch(config, config_entry, discovery_data)])
|
||||
async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)])
|
||||
|
||||
|
||||
class MqttSwitch(
|
||||
@@ -113,8 +113,9 @@ class MqttSwitch(
|
||||
):
|
||||
"""Representation of a switch that can be toggled using MQTT."""
|
||||
|
||||
def __init__(self, config, config_entry, discovery_data):
|
||||
def __init__(self, hass, config, config_entry, discovery_data):
|
||||
"""Initialize the MQTT switch."""
|
||||
self.hass = hass
|
||||
self._state = False
|
||||
self._sub_state = None
|
||||
|
||||
@@ -160,17 +161,19 @@ class MqttSwitch(
|
||||
|
||||
self._optimistic = config[CONF_OPTIMISTIC]
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if template is not None:
|
||||
template.hass = self.hass
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def state_message_received(msg):
|
||||
"""Handle new MQTT state messages."""
|
||||
payload = msg.payload
|
||||
template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if template is not None:
|
||||
payload = template.async_render_with_possible_json_value(payload)
|
||||
if payload == self._state_on:
|
||||
|
||||
@@ -284,9 +284,9 @@ class NetatmoCamera(NetatmoBase, Camera):
|
||||
self._data.events.get(self._id, {})
|
||||
)
|
||||
elif self._model == "NOC": # Smart Outdoor Camera
|
||||
self.hass.data[DOMAIN][DATA_EVENTS][
|
||||
self._id
|
||||
] = self._data.outdoor_events.get(self._id, {})
|
||||
self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events(
|
||||
self._data.outdoor_events.get(self._id, {})
|
||||
)
|
||||
|
||||
def process_events(self, events):
|
||||
"""Add meta data to events."""
|
||||
|
||||
@@ -5,6 +5,7 @@ import re
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_CLASS_DIRECTORY,
|
||||
MEDIA_CLASS_VIDEO,
|
||||
MEDIA_TYPE_VIDEO,
|
||||
)
|
||||
@@ -79,8 +80,20 @@ class NetatmoSource(MediaSource):
|
||||
) -> BrowseMediaSource:
|
||||
if event_id and event_id in self.events[camera_id]:
|
||||
created = dt.datetime.fromtimestamp(event_id)
|
||||
thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url")
|
||||
message = remove_html_tags(self.events[camera_id][event_id]["message"])
|
||||
if self.events[camera_id][event_id]["type"] == "outdoor":
|
||||
thumbnail = (
|
||||
self.events[camera_id][event_id]["event_list"][0]
|
||||
.get("snapshot", {})
|
||||
.get("url")
|
||||
)
|
||||
message = remove_html_tags(
|
||||
self.events[camera_id][event_id]["event_list"][0]["message"]
|
||||
)
|
||||
else:
|
||||
thumbnail = (
|
||||
self.events[camera_id][event_id].get("snapshot", {}).get("url")
|
||||
)
|
||||
message = remove_html_tags(self.events[camera_id][event_id]["message"])
|
||||
title = f"{created} - {message}"
|
||||
else:
|
||||
title = self.hass.data[DOMAIN][DATA_CAMERAS].get(camera_id, MANUFACTURER)
|
||||
@@ -91,10 +104,12 @@ class NetatmoSource(MediaSource):
|
||||
else:
|
||||
path = f"{source}/{camera_id}"
|
||||
|
||||
media_class = MEDIA_CLASS_DIRECTORY if event_id is None else MEDIA_CLASS_VIDEO
|
||||
|
||||
media = BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=path,
|
||||
media_class=MEDIA_CLASS_VIDEO,
|
||||
media_class=media_class,
|
||||
media_content_type=MEDIA_TYPE_VIDEO,
|
||||
title=title,
|
||||
can_play=bool(
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]"
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
@@ -39,4 +40,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,6 @@ def setup(hass, config):
|
||||
_LOGGER.error("Nextcloud setup failed - Check configuration")
|
||||
|
||||
hass.data[DOMAIN] = get_data_points(ncm.data)
|
||||
hass.data[DOMAIN]["instance"] = conf[CONF_URL]
|
||||
|
||||
def nextcloud_update(event_time):
|
||||
"""Update data from nextcloud api."""
|
||||
@@ -111,6 +110,7 @@ def setup(hass, config):
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN] = get_data_points(ncm.data)
|
||||
hass.data[DOMAIN]["instance"] = conf[CONF_URL]
|
||||
|
||||
# Update sensors on time interval
|
||||
track_time_interval(hass, nextcloud_update, conf[CONF_SCAN_INTERVAL])
|
||||
|
||||
@@ -38,8 +38,8 @@ def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]:
|
||||
"""
|
||||
nzbget_api = NZBGetAPI(
|
||||
data[CONF_HOST],
|
||||
data[CONF_USERNAME] if data[CONF_USERNAME] != "" else None,
|
||||
data[CONF_PASSWORD] if data[CONF_PASSWORD] != "" else None,
|
||||
data.get(CONF_USERNAME),
|
||||
data.get(CONF_PASSWORD),
|
||||
data[CONF_SSL],
|
||||
data[CONF_VERIFY_SSL],
|
||||
data[CONF_PORT],
|
||||
|
||||
@@ -29,8 +29,8 @@ class NZBGetDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Initialize global NZBGet data updater."""
|
||||
self.nzbget = NZBGetAPI(
|
||||
config[CONF_HOST],
|
||||
config[CONF_USERNAME] if config[CONF_USERNAME] != "" else None,
|
||||
config[CONF_PASSWORD] if config[CONF_PASSWORD] != "" else None,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
config[CONF_SSL],
|
||||
config[CONF_VERIFY_SSL],
|
||||
config[CONF_PORT],
|
||||
|
||||
@@ -91,7 +91,7 @@ class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if CONF_LONGITUDE not in config:
|
||||
config[CONF_LONGITUDE] = self.hass.config.longitude
|
||||
if CONF_MODE not in config:
|
||||
config[CONF_MODE] = DEFAULT_LANGUAGE
|
||||
config[CONF_MODE] = DEFAULT_FORECAST_MODE
|
||||
if CONF_LANGUAGE not in config:
|
||||
config[CONF_LANGUAGE] = DEFAULT_LANGUAGE
|
||||
return await self.async_step_user(config)
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.components.media_player import (
|
||||
)
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_CLASS_CHANNEL,
|
||||
MEDIA_CLASS_CHANNELS,
|
||||
MEDIA_CLASS_DIRECTORY,
|
||||
MEDIA_TYPE_CHANNEL,
|
||||
MEDIA_TYPE_CHANNELS,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
@@ -290,7 +290,7 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity):
|
||||
|
||||
return BrowseMedia(
|
||||
title="Channels",
|
||||
media_class=MEDIA_CLASS_CHANNELS,
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
media_content_id="",
|
||||
media_content_type=MEDIA_TYPE_CHANNELS,
|
||||
can_play=False,
|
||||
|
||||
@@ -1,4 +1,28 @@
|
||||
"""The ping component."""
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
||||
DOMAIN = "ping"
|
||||
PLATFORMS = ["binary_sensor"]
|
||||
|
||||
PING_ID = "ping_id"
|
||||
DEFAULT_START_ID = 129
|
||||
MAX_PING_ID = 65534
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_next_ping_id(hass):
|
||||
"""Find the next id to use in the outbound ping.
|
||||
|
||||
Must be called in async
|
||||
"""
|
||||
current_id = hass.data.setdefault(DOMAIN, {}).get(PING_ID, DEFAULT_START_ID)
|
||||
|
||||
if current_id == MAX_PING_ID:
|
||||
next_id = DEFAULT_START_ID
|
||||
else:
|
||||
next_id = current_id + 1
|
||||
|
||||
hass.data[DOMAIN][PING_ID] = next_id
|
||||
|
||||
return next_id
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tracks the latency of a host by sending ICMP echo requests (ping)."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
@@ -14,7 +15,7 @@ from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.reload import setup_reload_service
|
||||
|
||||
from . import DOMAIN, PLATFORMS
|
||||
from . import DOMAIN, PLATFORMS, async_get_next_ping_id
|
||||
from .const import PING_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -131,20 +132,28 @@ class PingData:
|
||||
class PingDataICMPLib(PingData):
|
||||
"""The Class for handling the data retrieval using icmplib."""
|
||||
|
||||
def ping(self):
|
||||
"""Send ICMP echo request and return details."""
|
||||
return icmp_ping(self._ip_address, count=self._count)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Retrieve the latest details from the host."""
|
||||
data = await self.hass.async_add_executor_job(self.ping)
|
||||
_LOGGER.debug("ping address: %s", self._ip_address)
|
||||
data = await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
icmp_ping,
|
||||
self._ip_address,
|
||||
count=self._count,
|
||||
id=async_get_next_ping_id(self.hass),
|
||||
)
|
||||
)
|
||||
self.available = data.is_alive
|
||||
if not self.available:
|
||||
self.data = False
|
||||
return
|
||||
|
||||
self.data = {
|
||||
"min": data.min_rtt,
|
||||
"max": data.max_rtt,
|
||||
"avg": data.avg_rtt,
|
||||
"mdev": "",
|
||||
}
|
||||
self.available = data.is_alive
|
||||
|
||||
|
||||
class PingDataSubProcess(PingData):
|
||||
@@ -201,7 +210,8 @@ class PingDataSubProcess(PingData):
|
||||
out_error,
|
||||
)
|
||||
|
||||
if pinger.returncode != 0:
|
||||
if pinger.returncode > 1:
|
||||
# returncode of 1 means the host is unreachable
|
||||
_LOGGER.exception(
|
||||
"Error running command: `%s`, return code: %s",
|
||||
" ".join(self._ping_cmd),
|
||||
|
||||
@@ -15,8 +15,10 @@ from homeassistant.components.device_tracker.const import (
|
||||
SOURCE_TYPE_ROUTER,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
from homeassistant.util.process import kill_subprocess
|
||||
|
||||
from . import async_get_next_ping_id
|
||||
from .const import PING_ATTEMPTS_COUNT, PING_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -76,15 +78,22 @@ class HostSubProcess:
|
||||
class HostICMPLib:
|
||||
"""Host object with ping detection."""
|
||||
|
||||
def __init__(self, ip_address, dev_id, _, config):
|
||||
def __init__(self, ip_address, dev_id, hass, config):
|
||||
"""Initialize the Host pinger."""
|
||||
self.hass = hass
|
||||
self.ip_address = ip_address
|
||||
self.dev_id = dev_id
|
||||
self._count = config[CONF_PING_COUNT]
|
||||
|
||||
def ping(self):
|
||||
"""Send an ICMP echo request and return True if success."""
|
||||
return icmp_ping(self.ip_address, count=PING_ATTEMPTS_COUNT).is_alive
|
||||
next_id = run_callback_threadsafe(
|
||||
self.hass.loop, async_get_next_ping_id, self.hass
|
||||
).result()
|
||||
|
||||
return icmp_ping(
|
||||
self.ip_address, count=PING_ATTEMPTS_COUNT, id=next_id
|
||||
).is_alive
|
||||
|
||||
def update(self, see):
|
||||
"""Update device state by sending one or more ping messages."""
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"name": "Ping (ICMP)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/ping",
|
||||
"codeowners": [],
|
||||
"requirements": ["icmplib==1.1.1"],
|
||||
"requirements": ["icmplib==1.1.3"],
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class UnknownMediaType(BrowseError):
|
||||
EXPANDABLES = ["album", "artist", "playlist", "season", "show"]
|
||||
PLAYLISTS_BROWSE_PAYLOAD = {
|
||||
"title": "Playlists",
|
||||
"media_class": MEDIA_CLASS_PLAYLIST,
|
||||
"media_class": MEDIA_CLASS_DIRECTORY,
|
||||
"media_content_id": "all",
|
||||
"media_content_type": "playlists",
|
||||
"can_play": False,
|
||||
@@ -94,10 +94,21 @@ def browse_media(
|
||||
if special_folder:
|
||||
if media_content_type == "server":
|
||||
library_or_section = plex_server.library
|
||||
children_media_class = MEDIA_CLASS_DIRECTORY
|
||||
title = plex_server.friendly_name
|
||||
elif media_content_type == "library":
|
||||
library_or_section = plex_server.library.sectionByID(media_content_id)
|
||||
title = library_or_section.title
|
||||
try:
|
||||
children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE]
|
||||
except KeyError as err:
|
||||
raise BrowseError(
|
||||
f"Media not found: {media_content_type} / {media_content_id}"
|
||||
) from err
|
||||
else:
|
||||
raise BrowseError(
|
||||
f"Media not found: {media_content_type} / {media_content_id}"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"title": title,
|
||||
@@ -107,6 +118,7 @@ def browse_media(
|
||||
"can_play": False,
|
||||
"can_expand": True,
|
||||
"children": [],
|
||||
"children_media_class": children_media_class,
|
||||
}
|
||||
|
||||
method = SPECIAL_METHODS[special_folder]
|
||||
@@ -116,13 +128,20 @@ def browse_media(
|
||||
payload["children"].append(item_payload(item))
|
||||
except UnknownMediaType:
|
||||
continue
|
||||
|
||||
return BrowseMedia(**payload)
|
||||
|
||||
if media_content_type in ["server", None]:
|
||||
return server_payload(plex_server)
|
||||
try:
|
||||
if media_content_type in ["server", None]:
|
||||
return server_payload(plex_server)
|
||||
|
||||
if media_content_type == "library":
|
||||
return library_payload(plex_server, media_content_id)
|
||||
if media_content_type == "library":
|
||||
return library_payload(plex_server, media_content_id)
|
||||
|
||||
except UnknownMediaType as err:
|
||||
raise BrowseError(
|
||||
f"Media not found: {media_content_type} / {media_content_id}"
|
||||
) from err
|
||||
|
||||
if media_content_type == "playlists":
|
||||
return playlists_payload(plex_server)
|
||||
@@ -160,6 +179,11 @@ def item_payload(item):
|
||||
|
||||
def library_section_payload(section):
|
||||
"""Create response payload for a single library section."""
|
||||
try:
|
||||
children_media_class = ITEM_TYPE_MEDIA_CLASS[section.TYPE]
|
||||
except KeyError as err:
|
||||
_LOGGER.debug("Unknown type received: %s", section.TYPE)
|
||||
raise UnknownMediaType from err
|
||||
return BrowseMedia(
|
||||
title=section.title,
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
@@ -167,6 +191,7 @@ def library_section_payload(section):
|
||||
media_content_type="library",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children_media_class=children_media_class,
|
||||
)
|
||||
|
||||
|
||||
@@ -180,6 +205,7 @@ def special_library_payload(parent_payload, special_type):
|
||||
media_content_type=parent_payload.media_content_type,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children_media_class=parent_payload.children_media_class,
|
||||
)
|
||||
|
||||
|
||||
@@ -194,6 +220,7 @@ def server_payload(plex_server):
|
||||
can_expand=True,
|
||||
)
|
||||
server_info.children = []
|
||||
server_info.children_media_class = MEDIA_CLASS_DIRECTORY
|
||||
server_info.children.append(special_library_payload(server_info, "On Deck"))
|
||||
server_info.children.append(special_library_payload(server_info, "Recently Added"))
|
||||
for library in plex_server.library.sections():
|
||||
@@ -229,4 +256,6 @@ def playlists_payload(plex_server):
|
||||
playlists_info["children"].append(item_payload(playlist))
|
||||
except UnknownMediaType:
|
||||
continue
|
||||
return BrowseMedia(**playlists_info)
|
||||
response = BrowseMedia(**playlists_info)
|
||||
response.children_media_class = MEDIA_CLASS_PLAYLIST
|
||||
return response
|
||||
|
||||
@@ -94,6 +94,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
api.get_all_devices()
|
||||
|
||||
if entry.unique_id is None:
|
||||
if api.smile_version[0] != "1.8.0":
|
||||
hass.config_entries.async_update_entry(entry, unique_id=api.smile_hostname)
|
||||
|
||||
undo_listener = entry.add_update_listener(_update_listener)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
|
||||
@@ -96,6 +96,10 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if self.discovery_info:
|
||||
user_input[CONF_HOST] = self.discovery_info[CONF_HOST]
|
||||
|
||||
for entry in self._async_current_entries():
|
||||
if entry.data.get(CONF_HOST) == user_input[CONF_HOST]:
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
try:
|
||||
api = await validate_input(self.hass, user_input)
|
||||
|
||||
@@ -106,9 +110,10 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if not errors:
|
||||
await self.async_set_unique_id(api.gateway_id)
|
||||
await self.async_set_unique_id(
|
||||
api.smile_hostname or api.gateway_id, raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(title=api.smile_name, data=user_input)
|
||||
|
||||
@@ -20,4 +20,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if entry and import_config.items() != entry.data.items():
|
||||
self.hass.config_entries.async_update_entry(entry, data=import_config)
|
||||
return self.async_abort(reason="already_configured")
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title="RFXTRX", data=import_config)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/risco",
|
||||
"requirements": [
|
||||
"pyrisco==0.3.0"
|
||||
"pyrisco==0.3.1"
|
||||
],
|
||||
"codeowners": [
|
||||
"@OnFreund"
|
||||
|
||||
154
homeassistant/components/roku/browse_media.py
Normal file
154
homeassistant/components/roku/browse_media.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Support for media browsing."""
|
||||
|
||||
from homeassistant.components.media_player import BrowseMedia
|
||||
from homeassistant.components.media_player.const import (
|
||||
MEDIA_CLASS_APP,
|
||||
MEDIA_CLASS_CHANNEL,
|
||||
MEDIA_CLASS_DIRECTORY,
|
||||
MEDIA_TYPE_APP,
|
||||
MEDIA_TYPE_APPS,
|
||||
MEDIA_TYPE_CHANNEL,
|
||||
MEDIA_TYPE_CHANNELS,
|
||||
)
|
||||
|
||||
CONTENT_TYPE_MEDIA_CLASS = {
|
||||
MEDIA_TYPE_APP: MEDIA_CLASS_APP,
|
||||
MEDIA_TYPE_APPS: MEDIA_CLASS_DIRECTORY,
|
||||
MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL,
|
||||
MEDIA_TYPE_CHANNELS: MEDIA_CLASS_DIRECTORY,
|
||||
}
|
||||
|
||||
PLAYABLE_MEDIA_TYPES = [
|
||||
MEDIA_TYPE_APP,
|
||||
MEDIA_TYPE_CHANNEL,
|
||||
]
|
||||
|
||||
EXPANDABLE_MEDIA_TYPES = [
|
||||
MEDIA_TYPE_APPS,
|
||||
MEDIA_TYPE_CHANNELS,
|
||||
]
|
||||
|
||||
|
||||
def build_item_response(coordinator, payload):
|
||||
"""Create response payload for the provided media query."""
|
||||
search_id = payload["search_id"]
|
||||
search_type = payload["search_type"]
|
||||
|
||||
thumbnail = None
|
||||
title = None
|
||||
media = None
|
||||
|
||||
if search_type == MEDIA_TYPE_APPS:
|
||||
title = "Apps"
|
||||
media = [
|
||||
{"app_id": item.app_id, "title": item.name, "type": MEDIA_TYPE_APP}
|
||||
for item in coordinator.data.apps
|
||||
]
|
||||
elif search_type == MEDIA_TYPE_CHANNELS:
|
||||
title = "Channels"
|
||||
media = [
|
||||
{
|
||||
"channel_number": item.number,
|
||||
"title": item.name,
|
||||
"type": MEDIA_TYPE_CHANNEL,
|
||||
}
|
||||
for item in coordinator.data.channels
|
||||
]
|
||||
|
||||
if media is None:
|
||||
return None
|
||||
|
||||
return BrowseMedia(
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
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, coordinator) for item in media],
|
||||
thumbnail=thumbnail,
|
||||
)
|
||||
|
||||
|
||||
def item_payload(item, coordinator):
|
||||
"""
|
||||
Create response payload for a single media item.
|
||||
|
||||
Used by async_browse_media.
|
||||
"""
|
||||
thumbnail = None
|
||||
|
||||
if "app_id" in item:
|
||||
media_content_type = MEDIA_TYPE_APP
|
||||
media_content_id = item["app_id"]
|
||||
thumbnail = coordinator.roku.app_icon_url(item["app_id"])
|
||||
elif "channel_number" in item:
|
||||
media_content_type = MEDIA_TYPE_CHANNEL
|
||||
media_content_id = item["channel_number"]
|
||||
else:
|
||||
media_content_type = item["type"]
|
||||
media_content_id = ""
|
||||
|
||||
title = item["title"]
|
||||
can_play = media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id
|
||||
can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES
|
||||
|
||||
return BrowseMedia(
|
||||
title=title,
|
||||
media_class=CONTENT_TYPE_MEDIA_CLASS[media_content_type],
|
||||
media_content_type=media_content_type,
|
||||
media_content_id=media_content_id,
|
||||
can_play=can_play,
|
||||
can_expand=can_expand,
|
||||
thumbnail=thumbnail,
|
||||
)
|
||||
|
||||
|
||||
def library_payload(coordinator):
|
||||
"""
|
||||
Create response payload to describe contents of a specific 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",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=[],
|
||||
)
|
||||
|
||||
library = {
|
||||
MEDIA_TYPE_APPS: "Apps",
|
||||
MEDIA_TYPE_CHANNELS: "Channels",
|
||||
}
|
||||
|
||||
for item in [{"title": name, "type": type_} for type_, name in library.items()]:
|
||||
if (
|
||||
item["type"] == MEDIA_TYPE_CHANNELS
|
||||
and coordinator.data.info.device_type != "tv"
|
||||
):
|
||||
continue
|
||||
|
||||
library_info.children.append(
|
||||
item_payload(
|
||||
{"title": item["title"], "type": item["type"]},
|
||||
coordinator,
|
||||
)
|
||||
)
|
||||
|
||||
if all(
|
||||
child.media_content_type == MEDIA_TYPE_APPS for child in library_info.children
|
||||
):
|
||||
library_info.children_media_class = MEDIA_CLASS_APP
|
||||
elif all(
|
||||
child.media_content_type == MEDIA_TYPE_CHANNELS
|
||||
for child in library_info.children
|
||||
):
|
||||
library_info.children_media_class = MEDIA_CLASS_CHANNEL
|
||||
else:
|
||||
library_info.children_media_class = MEDIA_CLASS_DIRECTORY
|
||||
|
||||
return library_info
|
||||
@@ -7,19 +7,11 @@ import voluptuous as vol
|
||||
from homeassistant.components.media_player import (
|
||||
DEVICE_CLASS_RECEIVER,
|
||||
DEVICE_CLASS_TV,
|
||||
BrowseMedia,
|
||||
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,
|
||||
MEDIA_TYPE_CHANNELS,
|
||||
SUPPORT_BROWSE_MEDIA,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
@@ -44,6 +36,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers import entity_platform
|
||||
|
||||
from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler
|
||||
from .browse_media import build_item_response, library_payload
|
||||
from .const import ATTR_KEYWORD, DOMAIN, SERVICE_SEARCH
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -80,44 +73,6 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
can_expand=True,
|
||||
children=[],
|
||||
)
|
||||
|
||||
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,
|
||||
can_play=False,
|
||||
)
|
||||
)
|
||||
|
||||
if channels:
|
||||
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,
|
||||
can_play=False,
|
||||
)
|
||||
)
|
||||
|
||||
return library_info
|
||||
|
||||
|
||||
class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
"""Representation of a Roku media player on the network."""
|
||||
|
||||
@@ -286,53 +241,13 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
||||
"""Implement the websocket media browsing helper."""
|
||||
if media_content_type in [None, "library"]:
|
||||
is_tv = self.coordinator.data.info.device_type == "tv"
|
||||
return browse_media_library(channels=is_tv)
|
||||
return library_payload(self.coordinator)
|
||||
|
||||
response = None
|
||||
|
||||
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,
|
||||
can_play=False,
|
||||
children=[
|
||||
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,
|
||||
can_expand=False,
|
||||
)
|
||||
for app in self.coordinator.data.apps
|
||||
],
|
||||
)
|
||||
|
||||
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,
|
||||
can_play=False,
|
||||
children=[
|
||||
BrowseMedia(
|
||||
title=channel.name,
|
||||
media_class=MEDIA_CLASS_CHANNEL,
|
||||
media_content_id=channel.number,
|
||||
media_content_type=MEDIA_TYPE_CHANNEL,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
)
|
||||
for channel in self.coordinator.data.channels
|
||||
],
|
||||
)
|
||||
payload = {
|
||||
"search_type": media_content_type,
|
||||
"search_id": media_content_id,
|
||||
}
|
||||
response = build_item_response(self.coordinator, payload)
|
||||
|
||||
if response is None:
|
||||
raise BrowseError(
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.const import (
|
||||
CONF_ICON,
|
||||
CONF_MODE,
|
||||
CONF_SEQUENCE,
|
||||
CONF_VARIABLES,
|
||||
SERVICE_RELOAD,
|
||||
SERVICE_TOGGLE,
|
||||
SERVICE_TURN_OFF,
|
||||
@@ -59,6 +60,7 @@ SCRIPT_ENTRY_SCHEMA = make_script_schema(
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_DESCRIPTION, default=""): cv.string,
|
||||
vol.Optional(CONF_VARIABLES): cv.SCRIPT_VARIABLES_SCHEMA,
|
||||
vol.Optional(CONF_FIELDS, default={}): {
|
||||
cv.string: {
|
||||
vol.Optional(CONF_DESCRIPTION): cv.string,
|
||||
@@ -75,7 +77,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
SCRIPT_SERVICE_SCHEMA = vol.Schema(dict)
|
||||
SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema(
|
||||
{vol.Optional(ATTR_VARIABLES): dict}
|
||||
{vol.Optional(ATTR_VARIABLES): {str: cv.match_all}}
|
||||
)
|
||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||
|
||||
@@ -263,6 +265,7 @@ class ScriptEntity(ToggleEntity):
|
||||
max_runs=cfg[CONF_MAX],
|
||||
max_exceeded=cfg[CONF_MAX_EXCEEDED],
|
||||
logger=logging.getLogger(f"{__name__}.{object_id}"),
|
||||
variables=cfg.get(CONF_VARIABLES),
|
||||
)
|
||||
self._changed = asyncio.Event()
|
||||
|
||||
|
||||
@@ -42,11 +42,11 @@ async def async_setup_entry_attribute_entities(
|
||||
if not blocks:
|
||||
return
|
||||
|
||||
counts = Counter([item[0].type for item in blocks])
|
||||
counts = Counter([item[1] for item in blocks])
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
sensor_class(wrapper, block, sensor_id, description, counts[block.type])
|
||||
sensor_class(wrapper, block, sensor_id, description, counts[sensor_id])
|
||||
for block, sensor_id, description in blocks
|
||||
]
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Shelly",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/shelly",
|
||||
"requirements": ["aioshelly==0.3.0"],
|
||||
"zeroconf": ["_http._tcp.local."],
|
||||
"requirements": ["aioshelly==0.3.2"],
|
||||
"zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }],
|
||||
"codeowners": ["@balloob", "@bieniu"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Switch for Shelly."""
|
||||
from aioshelly import RelayBlock
|
||||
from aioshelly import Block
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import callback
|
||||
@@ -13,6 +13,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up switches for device."""
|
||||
wrapper = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
# In roller mode the relay blocks exist but do not contain required info
|
||||
if (
|
||||
wrapper.model in ["SHSW-21", "SHSW-25"]
|
||||
and wrapper.device.settings["mode"] != "relay"
|
||||
):
|
||||
return
|
||||
|
||||
relay_blocks = [block for block in wrapper.device.blocks if block.type == "relay"]
|
||||
|
||||
if not relay_blocks:
|
||||
@@ -24,7 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
class RelaySwitch(ShellyBlockEntity, SwitchEntity):
|
||||
"""Switch that controls a relay block on Shelly devices."""
|
||||
|
||||
def __init__(self, wrapper: ShellyDeviceWrapper, block: RelayBlock) -> None:
|
||||
def __init__(self, wrapper: ShellyDeviceWrapper, block: Block) -> None:
|
||||
"""Initialize relay switch."""
|
||||
super().__init__(wrapper, block)
|
||||
self.control_result = None
|
||||
|
||||
@@ -202,22 +202,23 @@ class SlackNotificationService(BaseNotificationService):
|
||||
self, targets, message, title, blocks, username, icon
|
||||
):
|
||||
"""Send a text-only message."""
|
||||
if self._icon.lower().startswith(("http://", "https://")):
|
||||
icon_type = "url"
|
||||
else:
|
||||
icon_type = "emoji"
|
||||
message_dict = {
|
||||
"blocks": blocks,
|
||||
"link_names": True,
|
||||
"text": message,
|
||||
"username": username,
|
||||
}
|
||||
|
||||
if self._icon:
|
||||
if self._icon.lower().startswith(("http://", "https://")):
|
||||
icon_type = "url"
|
||||
else:
|
||||
icon_type = "emoji"
|
||||
|
||||
message_dict[f"icon_{icon_type}"] = icon
|
||||
|
||||
tasks = {
|
||||
target: self._client.chat_postMessage(
|
||||
**{
|
||||
"blocks": blocks,
|
||||
"channel": target,
|
||||
"link_names": True,
|
||||
"text": message,
|
||||
"username": username,
|
||||
f"icon_{icon_type}": icon,
|
||||
}
|
||||
)
|
||||
target: self._client.chat_postMessage(**message_dict, channel=target)
|
||||
for target in targets
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/smappee",
|
||||
"dependencies": ["http"],
|
||||
"requirements": [
|
||||
"pysmappee==0.2.10"
|
||||
"pysmappee==0.2.13"
|
||||
],
|
||||
"codeowners": [
|
||||
"@bsmappee"
|
||||
],
|
||||
"zeroconf": [
|
||||
"_ssh._tcp.local."
|
||||
{"type":"_ssh._tcp.local.", "name":"smappee1*"},
|
||||
{"type":"_ssh._tcp.local.", "name":"smappee2*"}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "Smappee: {name}",
|
||||
"step": {
|
||||
"environment": {
|
||||
"description": "Set up your Smappee to integrate with Home Assistant.",
|
||||
"data": {
|
||||
"environment": "Environment"
|
||||
}
|
||||
},
|
||||
"local": {
|
||||
"description": "Enter the host to initiate the Smappee local integration",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?",
|
||||
"title": "Discovered Smappee device"
|
||||
},
|
||||
"pick_implementation": {
|
||||
"title": "Pick Authentication Method"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.",
|
||||
"authorize_url_timeout": "Timeout generating authorize url.",
|
||||
"connection_error": "Failed to connect to Smappee device.",
|
||||
"missing_configuration": "The component is not configured. Please follow the documentation.",
|
||||
"invalid_mdns": "Unsupported device for the Smappee integration."
|
||||
"config": {
|
||||
"flow_title": "Smappee: {name}",
|
||||
"step": {
|
||||
"environment": {
|
||||
"description": "Set up your Smappee to integrate with Home Assistant.",
|
||||
"data": {
|
||||
"environment": "Environment"
|
||||
}
|
||||
},
|
||||
"local": {
|
||||
"description": "Enter the host to initiate the Smappee local integration",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to add the Smappee device with serialnumber `{serialnumber}` to Home Assistant?",
|
||||
"title": "Discovered Smappee device"
|
||||
},
|
||||
"pick_implementation": {
|
||||
"title": "Pick Authentication Method"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_configured_local_device": "Local device(s) is already configured. Please remove those first before configuring a cloud device.",
|
||||
"authorize_url_timeout": "Timeout generating authorize url.",
|
||||
"connection_error": "Failed to connect to Smappee device.",
|
||||
"missing_configuration": "The component is not configured. Please follow the documentation.",
|
||||
"invalid_mdns": "Unsupported device for the Smappee integration.",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"abort": {
|
||||
"already_setup": "You can only configure one Somfy account.",
|
||||
"authorize_url_timeout": "Timeout generating authorize url.",
|
||||
"missing_configuration": "The Somfy component is not configured. Please follow the documentation."
|
||||
"missing_configuration": "The Somfy component is not configured. Please follow the documentation.",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
|
||||
},
|
||||
"create_entry": { "default": "Successfully authenticated with Somfy." }
|
||||
}
|
||||
|
||||
@@ -222,6 +222,10 @@ ATTR_STATUS_LIGHT = "status_light"
|
||||
UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None}
|
||||
|
||||
|
||||
class UnknownMediaType(BrowseError):
|
||||
"""Unknown media type."""
|
||||
|
||||
|
||||
class SonosData:
|
||||
"""Storage class for platform global data."""
|
||||
|
||||
@@ -1487,7 +1491,20 @@ 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"]]]
|
||||
try:
|
||||
media_class = SONOS_TO_MEDIA_CLASSES[
|
||||
MEDIA_TYPES_TO_SONOS[payload["search_type"]]
|
||||
]
|
||||
except KeyError:
|
||||
_LOGGER.debug("Unknown media type received %s", payload["search_type"])
|
||||
return None
|
||||
|
||||
children = []
|
||||
for item in media:
|
||||
try:
|
||||
children.append(item_payload(item))
|
||||
except UnknownMediaType:
|
||||
pass
|
||||
|
||||
return BrowseMedia(
|
||||
title=title,
|
||||
@@ -1495,7 +1512,7 @@ def build_item_response(media_library, payload):
|
||||
media_class=media_class,
|
||||
media_content_id=payload["idstring"],
|
||||
media_content_type=payload["search_type"],
|
||||
children=[item_payload(item) for item in media],
|
||||
children=children,
|
||||
can_play=can_play(payload["search_type"]),
|
||||
can_expand=can_expand(payload["search_type"]),
|
||||
)
|
||||
@@ -1507,12 +1524,18 @@ def item_payload(item):
|
||||
|
||||
Used by async_browse_media.
|
||||
"""
|
||||
media_type = get_media_type(item)
|
||||
try:
|
||||
media_class = SONOS_TO_MEDIA_CLASSES[media_type]
|
||||
except KeyError as err:
|
||||
_LOGGER.debug("Unknown media type received %s", media_type)
|
||||
raise UnknownMediaType from err
|
||||
return BrowseMedia(
|
||||
title=item.title,
|
||||
thumbnail=getattr(item, "album_art_uri", None),
|
||||
media_class=SONOS_TO_MEDIA_CLASSES[get_media_type(item)],
|
||||
media_class=media_class,
|
||||
media_content_id=get_content_id(item),
|
||||
media_content_type=SONOS_TO_MEDIA_TYPES[get_media_type(item)],
|
||||
media_content_type=SONOS_TO_MEDIA_TYPES[media_type],
|
||||
can_play=can_play(item.item_class),
|
||||
can_expand=can_expand(item),
|
||||
)
|
||||
@@ -1524,6 +1547,20 @@ def library_payload(media_library):
|
||||
|
||||
Used by async_browse_media.
|
||||
"""
|
||||
if not media_library.browse_by_idstring(
|
||||
"tracks",
|
||||
"",
|
||||
max_items=1,
|
||||
):
|
||||
raise BrowseError("Local library not found")
|
||||
|
||||
children = []
|
||||
for item in media_library.browse():
|
||||
try:
|
||||
children.append(item_payload(item))
|
||||
except UnknownMediaType:
|
||||
pass
|
||||
|
||||
return BrowseMedia(
|
||||
title="Music Library",
|
||||
media_class=MEDIA_CLASS_DIRECTORY,
|
||||
@@ -1531,7 +1568,7 @@ def library_payload(media_library):
|
||||
media_content_type="library",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children=[item_payload(item) for item in media_library.browse()],
|
||||
children=children,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -143,9 +143,12 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator):
|
||||
|
||||
self.servers[DEFAULT_SERVER] = {}
|
||||
for server in sorted(
|
||||
server_list.values(), key=lambda server: server[0]["country"]
|
||||
server_list.values(),
|
||||
key=lambda server: server[0]["country"] + server[0]["sponsor"],
|
||||
):
|
||||
self.servers[f"{server[0]['country']} - {server[0]['sponsor']}"] = server[0]
|
||||
self.servers[
|
||||
f"{server[0]['country']} - {server[0]['sponsor']} - {server[0]['name']}"
|
||||
] = server[0]
|
||||
|
||||
def update_data(self):
|
||||
"""Get the latest data from speedtest.net."""
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.components.media_player.const import (
|
||||
MEDIA_CLASS_ARTIST,
|
||||
MEDIA_CLASS_DIRECTORY,
|
||||
MEDIA_CLASS_EPISODE,
|
||||
MEDIA_CLASS_GENRE,
|
||||
MEDIA_CLASS_PLAYLIST,
|
||||
MEDIA_CLASS_PODCAST,
|
||||
MEDIA_CLASS_TRACK,
|
||||
@@ -104,24 +105,57 @@ LIBRARY_MAP = {
|
||||
}
|
||||
|
||||
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,
|
||||
"current_user_playlists": {
|
||||
"parent": MEDIA_CLASS_DIRECTORY,
|
||||
"children": MEDIA_CLASS_PLAYLIST,
|
||||
},
|
||||
"current_user_followed_artists": {
|
||||
"parent": MEDIA_CLASS_DIRECTORY,
|
||||
"children": MEDIA_CLASS_ARTIST,
|
||||
},
|
||||
"current_user_saved_albums": {
|
||||
"parent": MEDIA_CLASS_DIRECTORY,
|
||||
"children": MEDIA_CLASS_ALBUM,
|
||||
},
|
||||
"current_user_saved_tracks": {
|
||||
"parent": MEDIA_CLASS_DIRECTORY,
|
||||
"children": MEDIA_CLASS_TRACK,
|
||||
},
|
||||
"current_user_saved_shows": {
|
||||
"parent": MEDIA_CLASS_DIRECTORY,
|
||||
"children": MEDIA_CLASS_PODCAST,
|
||||
},
|
||||
"current_user_recently_played": {
|
||||
"parent": MEDIA_CLASS_DIRECTORY,
|
||||
"children": MEDIA_CLASS_TRACK,
|
||||
},
|
||||
"current_user_top_artists": {
|
||||
"parent": MEDIA_CLASS_DIRECTORY,
|
||||
"children": MEDIA_CLASS_ARTIST,
|
||||
},
|
||||
"current_user_top_tracks": {
|
||||
"parent": MEDIA_CLASS_DIRECTORY,
|
||||
"children": MEDIA_CLASS_TRACK,
|
||||
},
|
||||
"featured_playlists": {
|
||||
"parent": MEDIA_CLASS_DIRECTORY,
|
||||
"children": MEDIA_CLASS_PLAYLIST,
|
||||
},
|
||||
"categories": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_GENRE},
|
||||
"category_playlists": {
|
||||
"parent": MEDIA_CLASS_DIRECTORY,
|
||||
"children": MEDIA_CLASS_PLAYLIST,
|
||||
},
|
||||
"new_releases": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ALBUM},
|
||||
MEDIA_TYPE_PLAYLIST: {
|
||||
"parent": MEDIA_CLASS_PLAYLIST,
|
||||
"children": MEDIA_CLASS_TRACK,
|
||||
},
|
||||
MEDIA_TYPE_ALBUM: {"parent": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_TRACK},
|
||||
MEDIA_TYPE_ARTIST: {"parent": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ALBUM},
|
||||
MEDIA_TYPE_EPISODE: {"parent": MEDIA_CLASS_EPISODE, "children": None},
|
||||
MEDIA_TYPE_SHOW: {"parent": MEDIA_CLASS_PODCAST, "children": MEDIA_CLASS_EPISODE},
|
||||
MEDIA_TYPE_TRACK: {"parent": MEDIA_CLASS_TRACK, "children": None},
|
||||
}
|
||||
|
||||
|
||||
@@ -542,7 +576,8 @@ def build_item_response(spotify, user, payload):
|
||||
if media_content_type == "categories":
|
||||
media_item = BrowseMedia(
|
||||
title=LIBRARY_MAP.get(media_content_id),
|
||||
media_class=media_class,
|
||||
media_class=media_class["parent"],
|
||||
children_media_class=media_class["children"],
|
||||
media_content_id=media_content_id,
|
||||
media_content_type=media_content_type,
|
||||
can_play=False,
|
||||
@@ -559,6 +594,7 @@ def build_item_response(spotify, user, payload):
|
||||
BrowseMedia(
|
||||
title=item.get("name"),
|
||||
media_class=MEDIA_CLASS_PLAYLIST,
|
||||
children_media_class=MEDIA_CLASS_TRACK,
|
||||
media_content_id=item_id,
|
||||
media_content_type="category_playlists",
|
||||
thumbnail=fetch_image_url(item, key="icons"),
|
||||
@@ -566,6 +602,7 @@ def build_item_response(spotify, user, payload):
|
||||
can_expand=True,
|
||||
)
|
||||
)
|
||||
return media_item
|
||||
|
||||
if title is None:
|
||||
if "name" in media:
|
||||
@@ -573,9 +610,10 @@ def build_item_response(spotify, user, payload):
|
||||
else:
|
||||
title = LIBRARY_MAP.get(payload["media_content_id"])
|
||||
|
||||
response = {
|
||||
params = {
|
||||
"title": title,
|
||||
"media_class": media_class,
|
||||
"media_class": media_class["parent"],
|
||||
"children_media_class": media_class["children"],
|
||||
"media_content_id": media_content_id,
|
||||
"media_content_type": media_content_type,
|
||||
"can_play": media_content_type in PLAYABLE_MEDIA_TYPES,
|
||||
@@ -584,16 +622,16 @@ def build_item_response(spotify, user, payload):
|
||||
}
|
||||
for item in items:
|
||||
try:
|
||||
response["children"].append(item_payload(item))
|
||||
params["children"].append(item_payload(item))
|
||||
except (MissingMediaInformation, UnknownMediaType):
|
||||
continue
|
||||
|
||||
if "images" in media:
|
||||
response["thumbnail"] = fetch_image_url(media)
|
||||
params["thumbnail"] = fetch_image_url(media)
|
||||
elif image:
|
||||
response["thumbnail"] = image
|
||||
params["thumbnail"] = image
|
||||
|
||||
return BrowseMedia(**response)
|
||||
return BrowseMedia(**params)
|
||||
|
||||
|
||||
def item_payload(item):
|
||||
@@ -622,17 +660,14 @@ def item_payload(item):
|
||||
|
||||
payload = {
|
||||
"title": item.get("name"),
|
||||
"media_class": media_class["parent"],
|
||||
"children_media_class": media_class["children"],
|
||||
"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:
|
||||
@@ -663,7 +698,9 @@ def library_payload():
|
||||
{"name": item["name"], "type": item["type"], "uri": item["type"]}
|
||||
)
|
||||
)
|
||||
return BrowseMedia(**library_info)
|
||||
response = BrowseMedia(**library_info)
|
||||
response.children_media_class = MEDIA_CLASS_DIRECTORY
|
||||
return response
|
||||
|
||||
|
||||
def fetch_image_url(item, key="images"):
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"abort": {
|
||||
"already_setup": "You can only configure one Spotify account.",
|
||||
"authorize_url_timeout": "Timeout generating authorize url.",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"missing_configuration": "The Spotify integration is not configured. Please follow the documentation.",
|
||||
"reauth_account_mismatch": "The Spotify account authenticated with, does not match the account needed re-authentication."
|
||||
},
|
||||
|
||||
@@ -122,6 +122,7 @@ class SQLSensor(Entity):
|
||||
def update(self):
|
||||
"""Retrieve sensor data from the query."""
|
||||
|
||||
data = None
|
||||
try:
|
||||
sess = self.sessionmaker()
|
||||
result = sess.execute(self._query)
|
||||
@@ -147,7 +148,7 @@ class SQLSensor(Entity):
|
||||
finally:
|
||||
sess.close()
|
||||
|
||||
if self._template is not None:
|
||||
if data is not None and self._template is not None:
|
||||
self._state = self._template.async_render_with_possible_json_value(
|
||||
data, None
|
||||
)
|
||||
|
||||
@@ -148,7 +148,8 @@ class HlsStreamOutput(StreamOutput):
|
||||
def container_options(self) -> Callable[[int], dict]:
|
||||
"""Return Callable which takes a sequence number and returns container options."""
|
||||
return lambda sequence: {
|
||||
"movflags": "frag_custom+empty_moov+default_base_moof+skip_sidx+frag_discont",
|
||||
# Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970
|
||||
"movflags": "frag_custom+empty_moov+default_base_moof+frag_discont",
|
||||
"avoid_negative_ts": "make_non_negative",
|
||||
"fragment_index": str(sequence),
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ def create_stream_buffer(stream_output, video_stream, audio_stream, sequence):
|
||||
segment,
|
||||
mode="w",
|
||||
format=stream_output.format,
|
||||
container_options=container_options,
|
||||
container_options={
|
||||
"video_track_timescale": str(int(1 / video_stream.time_base)),
|
||||
**container_options,
|
||||
},
|
||||
)
|
||||
vstream = output.add_stream(template=video_stream)
|
||||
# Check if audio is requested
|
||||
@@ -64,11 +67,16 @@ def _stream_worker_internal(hass, stream, quit_event):
|
||||
video_stream = container.streams.video[0]
|
||||
except (KeyError, IndexError):
|
||||
_LOGGER.error("Stream has no video")
|
||||
container.close()
|
||||
return
|
||||
try:
|
||||
audio_stream = container.streams.audio[0]
|
||||
except (KeyError, IndexError):
|
||||
audio_stream = None
|
||||
# These formats need aac_adtstoasc bitstream filter, but auto_bsf not
|
||||
# compatible with empty_moov and manual bitstream filters not in PyAV
|
||||
if container.format.name in {"hls", "mpegts"}:
|
||||
audio_stream = None
|
||||
|
||||
# The presentation timestamps of the first packet in each stream we receive
|
||||
# Use to adjust before muxing or outputting, but we don't adjust internally
|
||||
@@ -238,7 +246,7 @@ def _stream_worker_internal(hass, stream, quit_event):
|
||||
|
||||
# Update last_dts processed
|
||||
last_dts[packet.stream] = packet.dts
|
||||
# mux video packets immediately, save audio packets to be muxed all at once
|
||||
# mux packets
|
||||
if packet.stream == video_stream:
|
||||
mux_video_packet(packet) # mutates packet timestamps
|
||||
else:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user