mirror of
https://github.com/home-assistant/core.git
synced 2025-09-24 20:39:28 +00:00
Compare commits
75 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6c51ff63ad | ||
![]() |
aecc3476e8 | ||
![]() |
43a990350b | ||
![]() |
769c5c1779 | ||
![]() |
a93ae418a2 | ||
![]() |
562ccbbe25 | ||
![]() |
5f28c82837 | ||
![]() |
92f9a77770 | ||
![]() |
2430c5ea22 | ||
![]() |
6d54ed14b4 | ||
![]() |
faf73e304a | ||
![]() |
e902cfcbf0 | ||
![]() |
750bb11895 | ||
![]() |
2dcd57bf38 | ||
![]() |
26766d68aa | ||
![]() |
d26bceb110 | ||
![]() |
aa52ade5a0 | ||
![]() |
c57d697df5 | ||
![]() |
c604595f98 | ||
![]() |
1ab2d55a7a | ||
![]() |
048db9cb7d | ||
![]() |
0959777b81 | ||
![]() |
9c0a2d69ff | ||
![]() |
529b849efe | ||
![]() |
a34d06e363 | ||
![]() |
5b85776566 | ||
![]() |
68d2938c6b | ||
![]() |
5a460e609f | ||
![]() |
a88e011bba | ||
![]() |
40be42299d | ||
![]() |
7dcebbe89d | ||
![]() |
cb453d3ae1 | ||
![]() |
2effbb6604 | ||
![]() |
ba6acc286c | ||
![]() |
45c1c2acac | ||
![]() |
1962a7c7ce | ||
![]() |
8da732a89d | ||
![]() |
2697056ba9 | ||
![]() |
91e8593fb6 | ||
![]() |
9fa97473cb | ||
![]() |
29e7aa753c | ||
![]() |
af31aa7995 | ||
![]() |
e3d29429d3 | ||
![]() |
93e71ef218 | ||
![]() |
2893972c69 | ||
![]() |
13b8bc6290 | ||
![]() |
326d36d303 | ||
![]() |
287b33eef3 | ||
![]() |
3ec83dc29e | ||
![]() |
f716b7714b | ||
![]() |
b7c958719e | ||
![]() |
934ac73ae5 | ||
![]() |
bcd2ae78a0 | ||
![]() |
c00ab762d3 | ||
![]() |
1fb18580b2 | ||
![]() |
9c6351c1b3 | ||
![]() |
4eacf3f6c0 | ||
![]() |
35d2badb24 | ||
![]() |
331d5ba7ef | ||
![]() |
a2c157b5e9 | ||
![]() |
883fb8c168 | ||
![]() |
2e7ce63cbc | ||
![]() |
b925ae39cb | ||
![]() |
5c608eb1bc | ||
![]() |
3032f9280c | ||
![]() |
00284a87d0 | ||
![]() |
21917e86c9 | ||
![]() |
b6994689b1 | ||
![]() |
0c1c7d797c | ||
![]() |
9c60195780 | ||
![]() |
d8fea1c582 | ||
![]() |
84ab40c57d | ||
![]() |
aace9dce38 | ||
![]() |
f6f95d0422 | ||
![]() |
e0466d4ac8 |
@@ -161,7 +161,6 @@ homeassistant/components/goalzero/* @tkdrob
|
||||
homeassistant/components/gogogate2/* @vangorra
|
||||
homeassistant/components/google_assistant/* @home-assistant/cloud
|
||||
homeassistant/components/google_cloud/* @lufton
|
||||
homeassistant/components/google_translate/* @awarecan
|
||||
homeassistant/components/gpsd/* @fabaff
|
||||
homeassistant/components/gree/* @cmroche
|
||||
homeassistant/components/greeneye_monitor/* @jkeljo
|
||||
|
@@ -143,8 +143,8 @@ stages:
|
||||
|
||||
version="$(homeassistantRelease)"
|
||||
|
||||
git clone https://github.com/home-assistant/hassio-version
|
||||
cd hassio-version
|
||||
git clone https://github.com/home-assistant/version
|
||||
cd version
|
||||
|
||||
dev_version="$(jq --raw-output '.homeassistant.default' dev.json)"
|
||||
beta_version="$(jq --raw-output '.homeassistant.default' beta.json)"
|
||||
|
@@ -31,7 +31,7 @@ UPDATE_TOPIC = f"{DOMAIN}_update"
|
||||
|
||||
ICON = "mdi:package-variant-closed"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||
|
||||
SERVICE_ADD_TRACKING = "add_tracking"
|
||||
SERVICE_REMOVE_TRACKING = "remove_tracking"
|
||||
|
@@ -276,7 +276,7 @@ class Alert(ToggleEntity):
|
||||
self._send_done_message = True
|
||||
|
||||
if self._message_template is not None:
|
||||
message = self._message_template.async_render()
|
||||
message = self._message_template.async_render(parse_result=False)
|
||||
else:
|
||||
message = self._name
|
||||
|
||||
@@ -291,7 +291,7 @@ class Alert(ToggleEntity):
|
||||
if self._done_message_template is None:
|
||||
return
|
||||
|
||||
message = self._done_message_template.async_render()
|
||||
message = self._done_message_template.async_render(parse_result=False)
|
||||
|
||||
await self._send_notification_message(message)
|
||||
|
||||
@@ -300,7 +300,7 @@ class Alert(ToggleEntity):
|
||||
msg_payload = {ATTR_MESSAGE: message}
|
||||
|
||||
if self._title_template is not None:
|
||||
title = self._title_template.async_render()
|
||||
title = self._title_template.async_render(parse_result=False)
|
||||
msg_payload.update({ATTR_TITLE: title})
|
||||
if self._data:
|
||||
msg_payload.update({ATTR_DATA: self._data})
|
||||
|
@@ -80,13 +80,17 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||
output = {}
|
||||
if item.get(CONF_TITLE) is not None:
|
||||
if isinstance(item.get(CONF_TITLE), template.Template):
|
||||
output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render()
|
||||
output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render(
|
||||
parse_result=False
|
||||
)
|
||||
else:
|
||||
output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE)
|
||||
|
||||
if item.get(CONF_TEXT) is not None:
|
||||
if isinstance(item.get(CONF_TEXT), template.Template):
|
||||
output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render()
|
||||
output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render(
|
||||
parse_result=False
|
||||
)
|
||||
else:
|
||||
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
|
||||
|
||||
@@ -97,13 +101,17 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||
|
||||
if item.get(CONF_AUDIO) is not None:
|
||||
if isinstance(item.get(CONF_AUDIO), template.Template):
|
||||
output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render()
|
||||
output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render(
|
||||
parse_result=False
|
||||
)
|
||||
else:
|
||||
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
|
||||
|
||||
if item.get(CONF_DISPLAY_URL) is not None:
|
||||
if isinstance(item.get(CONF_DISPLAY_URL), template.Template):
|
||||
output[ATTR_REDIRECTION_URL] = item[CONF_DISPLAY_URL].async_render()
|
||||
output[ATTR_REDIRECTION_URL] = item[CONF_DISPLAY_URL].async_render(
|
||||
parse_result=False
|
||||
)
|
||||
else:
|
||||
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
||||
|
||||
|
@@ -271,7 +271,7 @@ class AlexaResponse:
|
||||
|
||||
self.reprompt = {
|
||||
"type": speech_type.value,
|
||||
key: text.async_render(self.variables),
|
||||
key: text.async_render(self.variables, parse_result=False),
|
||||
}
|
||||
|
||||
def as_dict(self):
|
||||
|
@@ -410,7 +410,7 @@ class APITemplateView(HomeAssistantView):
|
||||
try:
|
||||
data = await request.json()
|
||||
tpl = template.Template(data["template"], request.app["hass"])
|
||||
return str(tpl.async_render(data.get("variables")))
|
||||
return tpl.async_render(variables=data.get("variables"), parse_result=False)
|
||||
except (ValueError, TemplateError) as ex:
|
||||
return self.json_message(
|
||||
f"Error rendering template: {ex}", HTTP_BAD_REQUEST
|
||||
|
@@ -229,7 +229,7 @@ class ApnsNotificationService(BaseNotificationService):
|
||||
if isinstance(message, str):
|
||||
rendered_message = message
|
||||
elif isinstance(message, template_helper.Template):
|
||||
rendered_message = message.render()
|
||||
rendered_message = message.render(parse_result=False)
|
||||
else:
|
||||
rendered_message = ""
|
||||
|
||||
|
@@ -78,7 +78,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
def _render(value):
|
||||
try:
|
||||
return value_template.async_render({"value": value})
|
||||
return value_template.async_render({"value": value}, parse_result=False)
|
||||
except TemplateError:
|
||||
_LOGGER.exception("Error parsing value")
|
||||
return value
|
||||
|
@@ -280,12 +280,15 @@ async def get_device(hass, host, port, username, password):
|
||||
|
||||
except axis.Unauthorized as err:
|
||||
LOGGER.warning("Connected to device at %s but not registered.", host)
|
||||
await device.vapix.close()
|
||||
raise AuthenticationRequired from err
|
||||
|
||||
except (asyncio.TimeoutError, axis.RequestError) as err:
|
||||
LOGGER.error("Error connecting to the Axis device at %s", host)
|
||||
await device.vapix.close()
|
||||
raise CannotConnect from err
|
||||
|
||||
except axis.AxisException as err:
|
||||
LOGGER.exception("Unknown Axis communication error occurred")
|
||||
await device.vapix.close()
|
||||
raise AuthenticationRequired from err
|
||||
|
@@ -78,6 +78,7 @@ class CanaryCamera(CoordinatorEntity, Camera):
|
||||
def __init__(self, hass, coordinator, location_id, device, timeout, ffmpeg_args):
|
||||
"""Initialize a Canary security camera."""
|
||||
super().__init__(coordinator)
|
||||
Camera.__init__(self)
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = ffmpeg_args
|
||||
self._location_id = location_id
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"domain": "cloudflare",
|
||||
"name": "Cloudflare",
|
||||
"documentation": "https://www.home-assistant.io/integrations/cloudflare",
|
||||
"requirements": ["pycfdns==1.1.1"],
|
||||
"requirements": ["pycfdns==1.2.1"],
|
||||
"codeowners": ["@ludeeus", "@ctalkington"],
|
||||
"config_flow": true
|
||||
}
|
||||
|
@@ -161,7 +161,7 @@ class DialogflowResponse:
|
||||
assert self.speech is None
|
||||
|
||||
if isinstance(text, template.Template):
|
||||
text = text.async_render(self.parameters)
|
||||
text = text.async_render(self.parameters, parse_result=False)
|
||||
|
||||
self.speech = text
|
||||
|
||||
|
@@ -168,7 +168,7 @@ async def async_setup_platform(
|
||||
requester = AiohttpSessionRequester(session, True)
|
||||
|
||||
# ensure event handler has been started
|
||||
with await hass.data[DLNA_DMR_DATA]["lock"]:
|
||||
async with hass.data[DLNA_DMR_DATA]["lock"]:
|
||||
server_host = config.get(CONF_LISTEN_IP)
|
||||
if server_host is None:
|
||||
server_host = get_local_ip()
|
||||
@@ -220,7 +220,7 @@ class DlnaDmrDevice(MediaPlayerEntity):
|
||||
|
||||
async def _async_on_hass_stop(self, event):
|
||||
"""Event handler on Home Assistant stop."""
|
||||
with await self.hass.data[DLNA_DMR_DATA]["lock"]:
|
||||
async with self.hass.data[DLNA_DMR_DATA]["lock"]:
|
||||
await self._device.async_unsubscribe_services()
|
||||
|
||||
async def async_update(self):
|
||||
|
@@ -320,6 +320,16 @@ class DerivativeDSMREntity(DSMREntity):
|
||||
"""Return the calculated current hourly rate."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def force_update(self):
|
||||
"""Disable force update."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Enable polling."""
|
||||
return True
|
||||
|
||||
async def async_update(self):
|
||||
"""Recalculate hourly rate if timestamp has changed.
|
||||
|
||||
|
@@ -132,7 +132,10 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
|
||||
elif (
|
||||
self._temp_sensor_device is None
|
||||
and "unit" in device.properties
|
||||
and "value" in device.properties
|
||||
and (
|
||||
"value" in device.properties
|
||||
or "heatingThermostatSetpoint" in device.properties
|
||||
)
|
||||
and (device.properties.unit == "C" or device.properties.unit == "F")
|
||||
):
|
||||
self._temp_sensor_device = FibaroDevice(device)
|
||||
@@ -141,6 +144,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
|
||||
if (
|
||||
"setTargetLevel" in device.actions
|
||||
or "setThermostatSetpoint" in device.actions
|
||||
or "setHeatingThermostatSetpoint" in device.actions
|
||||
):
|
||||
self._target_temp_device = FibaroDevice(device)
|
||||
self._support_flags |= SUPPORT_TARGET_TEMPERATURE
|
||||
@@ -317,6 +321,8 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
|
||||
"""Return the current temperature."""
|
||||
if self._temp_sensor_device:
|
||||
device = self._temp_sensor_device.fibaro_device
|
||||
if "heatingThermostatSetpoint" in device.properties:
|
||||
return float(device.properties.heatingThermostatSetpoint)
|
||||
return float(device.properties.value)
|
||||
return None
|
||||
|
||||
@@ -325,6 +331,8 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self._target_temp_device:
|
||||
device = self._target_temp_device.fibaro_device
|
||||
if "heatingThermostatSetpointFuture" in device.properties:
|
||||
return float(device.properties.heatingThermostatSetpointFuture)
|
||||
return float(device.properties.targetLevel)
|
||||
return None
|
||||
|
||||
@@ -335,5 +343,7 @@ class FibaroThermostat(FibaroDevice, ClimateEntity):
|
||||
if temperature is not None:
|
||||
if "setThermostatSetpoint" in target.fibaro_device.actions:
|
||||
target.action("setThermostatSetpoint", self.fibaro_op_mode, temperature)
|
||||
elif "setHeatingThermostatSetpoint" in target.fibaro_device.actions:
|
||||
target.action("setHeatingThermostatSetpoint", temperature)
|
||||
else:
|
||||
target.action("setTargetLevel", temperature)
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20201021.2"],
|
||||
"requirements": ["home-assistant-frontend==20201021.4"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
@@ -121,7 +121,7 @@ class GenericCamera(Camera):
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
try:
|
||||
url = self._still_image_url.async_render()
|
||||
url = self._still_image_url.async_render(parse_result=False)
|
||||
except TemplateError as err:
|
||||
_LOGGER.error("Error parsing template %s: %s", self._still_image_url, err)
|
||||
return self._last_image
|
||||
@@ -178,7 +178,7 @@ class GenericCamera(Camera):
|
||||
return None
|
||||
|
||||
try:
|
||||
return self._stream_source.async_render()
|
||||
return self._stream_source.async_render(parse_result=False)
|
||||
except TemplateError as err:
|
||||
_LOGGER.error("Error parsing template %s: %s", self._stream_source, err)
|
||||
return None
|
||||
|
@@ -2,6 +2,6 @@
|
||||
"domain": "geo_rss_events",
|
||||
"name": "GeoRSS",
|
||||
"documentation": "https://www.home-assistant.io/integrations/geo_rss_events",
|
||||
"requirements": ["georss_generic_client==0.3"],
|
||||
"requirements": ["georss_generic_client==0.4"],
|
||||
"codeowners": ["@exxamalte"]
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from georss_client import UPDATE_OK, UPDATE_OK_NO_DATA
|
||||
from georss_client.generic_feed import GenericFeed
|
||||
from georss_generic_client import GenericFeed
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
|
@@ -3,5 +3,5 @@
|
||||
"name": "Google Translate Text-to-Speech",
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_translate",
|
||||
"requirements": ["gTTS-token==1.1.3"],
|
||||
"codeowners": ["@awarecan"]
|
||||
"codeowners": []
|
||||
}
|
||||
|
@@ -123,9 +123,14 @@ class GoogleProvider(Provider):
|
||||
|
||||
data = b""
|
||||
for idx, part in enumerate(message_parts):
|
||||
part_token = await self.hass.async_add_executor_job(
|
||||
token.calculate_token, part
|
||||
)
|
||||
try:
|
||||
part_token = await self.hass.async_add_executor_job(
|
||||
token.calculate_token, part
|
||||
)
|
||||
except ValueError as err:
|
||||
# If token seed fetching fails.
|
||||
_LOGGER.warning(err)
|
||||
return None, None
|
||||
|
||||
url_param = {
|
||||
"ie": "UTF-8",
|
||||
|
@@ -3,6 +3,6 @@
|
||||
"name": "Gree Climate",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/gree",
|
||||
"requirements": ["greeclimate==0.9.0"],
|
||||
"requirements": ["greeclimate==0.9.2"],
|
||||
"codeowners": ["@cmroche"]
|
||||
}
|
||||
|
@@ -2,6 +2,6 @@
|
||||
"domain": "hikvision",
|
||||
"name": "Hikvision",
|
||||
"documentation": "https://www.home-assistant.io/integrations/hikvision",
|
||||
"requirements": ["pyhik==0.2.7"],
|
||||
"requirements": ["pyhik==0.2.8"],
|
||||
"codeowners": ["@mezz64"]
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": [
|
||||
"aiohomekit==0.2.53"
|
||||
"aiohomekit==0.2.54"
|
||||
],
|
||||
"zeroconf": [
|
||||
"_hap._tcp.local."
|
||||
|
@@ -157,7 +157,9 @@ class HpIloSensor(Entity):
|
||||
ilo_data = getattr(self.hp_ilo_data.data, self._ilo_function)()
|
||||
|
||||
if self._sensor_value_template is not None:
|
||||
ilo_data = self._sensor_value_template.render(ilo_data=ilo_data)
|
||||
ilo_data = self._sensor_value_template.render(
|
||||
ilo_data=ilo_data, parse_result=False
|
||||
)
|
||||
|
||||
self._state = ilo_data
|
||||
|
||||
|
@@ -187,7 +187,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
|
||||
icloud_account = hass.data[DOMAIN].get(account_identifier)
|
||||
if icloud_account is None:
|
||||
for account in hass.data[DOMAIN].values():
|
||||
if account.name == account_identifier:
|
||||
if account.username == account_identifier:
|
||||
icloud_account = account
|
||||
|
||||
if icloud_account is None:
|
||||
|
@@ -183,7 +183,7 @@ class EmailContentSensor(Entity):
|
||||
ATTR_DATE: email_message["Date"],
|
||||
ATTR_BODY: EmailContentSensor.get_msg_text(email_message),
|
||||
}
|
||||
return self._value_template.render(variables)
|
||||
return self._value_template.render(variables, parse_result=False)
|
||||
|
||||
def sender_allowed(self, email_message):
|
||||
"""Check if the sender is in the allowed senders list."""
|
||||
|
@@ -26,6 +26,7 @@ from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import event as event_helper, state as state_helper
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_values import EntityValues
|
||||
@@ -500,6 +501,7 @@ class InfluxThread(threading.Thread):
|
||||
self.shutdown = False
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener)
|
||||
|
||||
@callback
|
||||
def _event_listener(self, event):
|
||||
"""Listen for new messages on the bus and queue them for Influx."""
|
||||
item = (time.monotonic(), event)
|
||||
|
@@ -268,7 +268,7 @@ class InfluxFluxSensorData:
|
||||
"""Get the latest data by querying influx."""
|
||||
_LOGGER.debug(RENDERING_QUERY_MESSAGE, self.query)
|
||||
try:
|
||||
rendered_query = self.query.render()
|
||||
rendered_query = self.query.render(parse_result=False)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(RENDERING_QUERY_ERROR_MESSAGE, ex)
|
||||
return
|
||||
@@ -312,7 +312,7 @@ class InfluxQLSensorData:
|
||||
"""Get the latest data with a shell command."""
|
||||
_LOGGER.debug(RENDERING_WHERE_MESSAGE, self.where)
|
||||
try:
|
||||
where_clause = self.where.render()
|
||||
where_clause = self.where.render(parse_result=False)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(RENDERING_WHERE_ERROR_MESSAGE, ex)
|
||||
return
|
||||
|
@@ -87,13 +87,14 @@ class ScriptIntentHandler(intent.IntentHandler):
|
||||
|
||||
if speech is not None:
|
||||
response.async_set_speech(
|
||||
speech[CONF_TEXT].async_render(slots), speech[CONF_TYPE]
|
||||
speech[CONF_TEXT].async_render(slots, parse_result=False),
|
||||
speech[CONF_TYPE],
|
||||
)
|
||||
|
||||
if card is not None:
|
||||
response.async_set_card(
|
||||
card[CONF_TITLE].async_render(slots),
|
||||
card[CONF_CONTENT].async_render(slots),
|
||||
card[CONF_TITLE].async_render(slots, parse_result=False),
|
||||
card[CONF_CONTENT].async_render(slots, parse_result=False),
|
||||
card[CONF_TYPE],
|
||||
)
|
||||
|
||||
|
@@ -2,6 +2,6 @@
|
||||
"domain": "jewish_calendar",
|
||||
"name": "Jewish Calendar",
|
||||
"documentation": "https://www.home-assistant.io/integrations/jewish_calendar",
|
||||
"requirements": ["hdate==0.9.5"],
|
||||
"requirements": ["hdate==0.9.12"],
|
||||
"codeowners": ["@tsvi"]
|
||||
}
|
||||
|
@@ -89,7 +89,7 @@ class LcnOutputsCover(LcnDevice, CoverEntity):
|
||||
self._is_opening = False
|
||||
self._is_closing = True
|
||||
state = pypck.lcn_defs.MotorStateModifier.DOWN
|
||||
self.address_connection.control_motors_outputs(state)
|
||||
self.address_connection.control_motors_outputs(state, self.reverse_time)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_open_cover(self, **kwargs):
|
||||
@@ -106,7 +106,7 @@ class LcnOutputsCover(LcnDevice, CoverEntity):
|
||||
self._is_closing = False
|
||||
self._is_opening = False
|
||||
state = pypck.lcn_defs.MotorStateModifier.STOP
|
||||
self.address_connection.control_motors_outputs(state, self.reverse_time)
|
||||
self.address_connection.control_motors_outputs(state)
|
||||
self.async_write_ha_state()
|
||||
|
||||
def input_received(self, input_obj):
|
||||
|
@@ -144,7 +144,7 @@ async def async_setup(hass, config):
|
||||
domain = DOMAIN
|
||||
|
||||
message.hass = hass
|
||||
message = message.async_render()
|
||||
message = message.async_render(parse_result=False)
|
||||
async_log_entry(hass, name, message, domain, entity_id)
|
||||
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"name": "Lutron Caséta",
|
||||
"documentation": "https://www.home-assistant.io/integrations/lutron_caseta",
|
||||
"requirements": [
|
||||
"pylutron-caseta==0.7.0"
|
||||
"pylutron-caseta==0.7.1"
|
||||
],
|
||||
"codeowners": [
|
||||
"@swails"
|
||||
|
@@ -385,7 +385,9 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity):
|
||||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.render(from_state=self._state, to_state=state)
|
||||
alarm_code = self._code.render(
|
||||
parse_result=False, from_state=self._state, to_state=state
|
||||
)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
|
@@ -406,7 +406,9 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity):
|
||||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.render(from_state=self._state, to_state=state)
|
||||
alarm_code = self._code.render(
|
||||
from_state=self._state, to_state=state, parse_result=False
|
||||
)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
|
@@ -21,6 +21,8 @@ DEFAULT_VOICE = "cmu-slt-hsmm"
|
||||
DEFAULT_CODEC = "WAVE_FILE"
|
||||
DEFAULT_EFFECTS = {}
|
||||
|
||||
MAP_MARYTTS_CODEC = {"WAVE_FILE": "wav", "AIFF_FILE": "aiff", "AU_FILE": "au"}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
@@ -81,5 +83,6 @@ class MaryTTSProvider(Provider):
|
||||
effects = options[CONF_EFFECT]
|
||||
|
||||
data = self._mary.speak(message, effects)
|
||||
audiotype = MAP_MARYTTS_CODEC[self._mary.codec]
|
||||
|
||||
return self._mary.codec, data
|
||||
return audiotype, data
|
||||
|
@@ -124,7 +124,7 @@ def setup(hass, config):
|
||||
def _render_service_value(service, key):
|
||||
value = service.data[key]
|
||||
value.hass = hass
|
||||
return value.async_render()
|
||||
return value.async_render(parse_result=False)
|
||||
|
||||
def put_file(service):
|
||||
"""Upload file service."""
|
||||
|
@@ -322,7 +322,7 @@ MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend(
|
||||
MQTT_PUBLISH_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_TOPIC): valid_publish_topic,
|
||||
vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): object,
|
||||
vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string,
|
||||
vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.string,
|
||||
vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
|
||||
vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
@@ -567,7 +567,9 @@ async def async_setup_entry(hass, entry):
|
||||
retain: bool = call.data[ATTR_RETAIN]
|
||||
if payload_template is not None:
|
||||
try:
|
||||
payload = template.Template(payload_template, hass).async_render()
|
||||
payload = template.Template(payload_template, hass).async_render(
|
||||
parse_result=False
|
||||
)
|
||||
except template.jinja2.TemplateError as exc:
|
||||
_LOGGER.error(
|
||||
"Unable to publish to %s: rendering payload template of "
|
||||
|
@@ -338,7 +338,7 @@ class MqttAlarm(
|
||||
"""Publish via mqtt."""
|
||||
command_template = self._config[CONF_COMMAND_TEMPLATE]
|
||||
values = {"action": action, "code": code}
|
||||
payload = command_template.async_render(**values)
|
||||
payload = command_template.async_render(**values, parse_result=False)
|
||||
mqtt.async_publish(
|
||||
self.hass,
|
||||
self._config[CONF_COMMAND_TOPIC],
|
||||
|
@@ -557,7 +557,7 @@ class MqttCover(
|
||||
position = kwargs[ATTR_POSITION]
|
||||
percentage_position = position
|
||||
if set_position_template is not None:
|
||||
position = set_position_template.async_render(**kwargs)
|
||||
position = set_position_template.async_render(parse_result=False, **kwargs)
|
||||
else:
|
||||
position = self.find_in_range_from_percent(position, COVER_PAYLOAD)
|
||||
|
||||
|
@@ -252,7 +252,7 @@ class MqttLightTemplate(
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid color value received")
|
||||
|
||||
if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None:
|
||||
if self._templates[CONF_WHITE_VALUE_TEMPLATE] is not None:
|
||||
try:
|
||||
self._white_value = int(
|
||||
self._templates[
|
||||
@@ -441,7 +441,9 @@ class MqttLightTemplate(
|
||||
mqtt.async_publish(
|
||||
self.hass,
|
||||
self._topics[CONF_COMMAND_TOPIC],
|
||||
self._templates[CONF_COMMAND_ON_TEMPLATE].async_render(**values),
|
||||
self._templates[CONF_COMMAND_ON_TEMPLATE].async_render(
|
||||
parse_result=False, **values
|
||||
),
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
)
|
||||
@@ -464,7 +466,9 @@ class MqttLightTemplate(
|
||||
mqtt.async_publish(
|
||||
self.hass,
|
||||
self._topics[CONF_COMMAND_TOPIC],
|
||||
self._templates[CONF_COMMAND_OFF_TEMPLATE].async_render(**values),
|
||||
self._templates[CONF_COMMAND_OFF_TEMPLATE].async_render(
|
||||
parse_result=False, **values
|
||||
),
|
||||
self._config[CONF_QOS],
|
||||
self._config[CONF_RETAIN],
|
||||
)
|
||||
|
@@ -30,7 +30,11 @@ from homeassistant.helpers import (
|
||||
config_entry_oauth2_flow,
|
||||
config_validation as cv,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from . import api, config_flow, local_auth
|
||||
@@ -176,7 +180,7 @@ class SignalUpdateCallback(EventCallback):
|
||||
# This event triggered an update to a device that changed some
|
||||
# properties which the DeviceManager should already have received.
|
||||
# Send a signal to refresh state of all listening devices.
|
||||
dispatcher_send(self._hass, SIGNAL_NEST_UPDATE)
|
||||
async_dispatcher_send(self._hass, SIGNAL_NEST_UPDATE)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
@@ -203,7 +207,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID]
|
||||
)
|
||||
subscriber.set_update_callback(SignalUpdateCallback(hass))
|
||||
hass.loop.create_task(subscriber.start_async())
|
||||
asyncio.create_task(subscriber.start_async())
|
||||
hass.data[DOMAIN][entry.entry_id] = subscriber
|
||||
|
||||
for component in PLATFORMS:
|
||||
|
@@ -61,6 +61,10 @@ class CodeInvalid(NestAuthError):
|
||||
"""Raised when invalid authorization code."""
|
||||
|
||||
|
||||
class UnexpectedStateError(HomeAssistantError):
|
||||
"""Raised when the config flow is invoked in a 'should not happen' case."""
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class NestFlowHandler(
|
||||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
|
||||
@@ -111,7 +115,7 @@ class NestFlowHandler(
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle a flow start."""
|
||||
if self.is_sdm_api():
|
||||
return None
|
||||
raise UnexpectedStateError("Step only supported for legacy API")
|
||||
|
||||
flows = self.hass.data.get(DATA_FLOW_IMPL, {})
|
||||
|
||||
@@ -142,7 +146,7 @@ class NestFlowHandler(
|
||||
deliver the authentication code.
|
||||
"""
|
||||
if self.is_sdm_api():
|
||||
return None
|
||||
raise UnexpectedStateError("Step only supported for legacy API")
|
||||
|
||||
flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl]
|
||||
|
||||
@@ -185,7 +189,7 @@ class NestFlowHandler(
|
||||
async def async_step_import(self, info):
|
||||
"""Import existing auth from Nest."""
|
||||
if self.is_sdm_api():
|
||||
return None
|
||||
raise UnexpectedStateError("Step only supported for legacy API")
|
||||
|
||||
if self.hass.config_entries.async_entries(DOMAIN):
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
@@ -13,5 +13,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the sensors."""
|
||||
if DATA_SDM not in entry.data:
|
||||
return await async_setup_legacy_entry(hass, entry, async_add_entities)
|
||||
return await async_setup_sdm_entry(hass, entry, async_add_entities)
|
||||
await async_setup_legacy_entry(hass, entry, async_add_entities)
|
||||
return
|
||||
|
||||
await async_setup_sdm_entry(hass, entry, async_add_entities)
|
||||
|
@@ -18,6 +18,13 @@ from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from .const import DOMAIN, SIGNAL_NEST_UPDATE
|
||||
|
||||
DEVICE_TYPE_MAP = {
|
||||
"sdm.devices.types.CAMERA": "Camera",
|
||||
"sdm.devices.types.DISPLAY": "Display",
|
||||
"sdm.devices.types.DOORBELL": "Doorbell",
|
||||
"sdm.devices.types.THERMOSTAT": "Thermostat",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_sdm_entry(
|
||||
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
|
||||
@@ -46,7 +53,7 @@ class SensorBase(Entity):
|
||||
self._device = device
|
||||
|
||||
@property
|
||||
def should_pool(self) -> bool:
|
||||
def should_poll(self) -> bool:
|
||||
"""Disable polling since entities have state pushed via pubsub."""
|
||||
return False
|
||||
|
||||
@@ -89,28 +96,19 @@ class SensorBase(Entity):
|
||||
# The API intentionally returns minimal information about specific
|
||||
# devices, instead relying on traits, but we can infer a generic model
|
||||
# name based on the type
|
||||
if self._device.type == "sdm.devices.types.CAMERA":
|
||||
return "Camera"
|
||||
if self._device.type == "sdm.devices.types.DISPLAY":
|
||||
return "Display"
|
||||
if self._device.type == "sdm.devices.types.DOORBELL":
|
||||
return "Doorbell"
|
||||
if self._device.type == "sdm.devices.types.THERMOSTAT":
|
||||
return "Thermostat"
|
||||
return None
|
||||
return DEVICE_TYPE_MAP.get(self._device.type)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity is added to register update signal handler."""
|
||||
|
||||
async def async_update_state():
|
||||
"""Update sensor state."""
|
||||
await self.async_update_ha_state(True)
|
||||
|
||||
# Event messages trigger the SIGNAL_NEST_UPDATE, which is intercepted
|
||||
# here to re-fresh the signals from _device. Unregister this callback
|
||||
# when the entity is removed.
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state)
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
SIGNAL_NEST_UPDATE,
|
||||
self.async_write_ha_state,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
@@ -25,7 +25,8 @@
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
"authorize_url_fail": "Unknown error generating an authorize url."
|
||||
"authorize_url_fail": "Unknown error generating an authorize url.",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
|
@@ -133,7 +133,7 @@ class BaseNotificationService:
|
||||
|
||||
if title:
|
||||
title.hass = self.hass
|
||||
kwargs[ATTR_TITLE] = title.async_render()
|
||||
kwargs[ATTR_TITLE] = title.async_render(parse_result=False)
|
||||
|
||||
if self._registered_targets.get(service.service) is not None:
|
||||
kwargs[ATTR_TARGET] = [self._registered_targets[service.service]]
|
||||
@@ -141,7 +141,7 @@ class BaseNotificationService:
|
||||
kwargs[ATTR_TARGET] = service.data.get(ATTR_TARGET)
|
||||
|
||||
message.hass = self.hass
|
||||
kwargs[ATTR_MESSAGE] = message.async_render()
|
||||
kwargs[ATTR_MESSAGE] = message.async_render(parse_result=False)
|
||||
kwargs[ATTR_DATA] = service.data.get(ATTR_DATA)
|
||||
|
||||
await self.async_send_message(**kwargs)
|
||||
@@ -229,12 +229,12 @@ async def async_setup(hass, config):
|
||||
payload = {}
|
||||
message = service.data[ATTR_MESSAGE]
|
||||
message.hass = hass
|
||||
payload[ATTR_MESSAGE] = message.async_render()
|
||||
payload[ATTR_MESSAGE] = message.async_render(parse_result=False)
|
||||
|
||||
title = service.data.get(ATTR_TITLE)
|
||||
if title:
|
||||
title.hass = hass
|
||||
payload[ATTR_TITLE] = title.async_render()
|
||||
payload[ATTR_TITLE] = title.async_render(parse_result=False)
|
||||
|
||||
await hass.services.async_call(
|
||||
pn.DOMAIN, pn.SERVICE_CREATE, payload, blocking=True
|
||||
|
@@ -74,6 +74,14 @@ class EventManager:
|
||||
async def async_start(self) -> bool:
|
||||
"""Start polling events."""
|
||||
if await self.device.create_pullpoint_subscription():
|
||||
# Create subscription manager
|
||||
self._subscription = self.device.create_subscription_service(
|
||||
"PullPointSubscription"
|
||||
)
|
||||
|
||||
# Renew immediately
|
||||
await self.async_renew()
|
||||
|
||||
# Initialize events
|
||||
pullpoint = self.device.create_pullpoint_service()
|
||||
try:
|
||||
@@ -87,11 +95,6 @@ class EventManager:
|
||||
# Parse event initialization
|
||||
await self.async_parse_messages(response.NotificationMessage)
|
||||
|
||||
# Create subscription manager
|
||||
self._subscription = self.device.create_subscription_service(
|
||||
"PullPointSubscription"
|
||||
)
|
||||
|
||||
self.started = True
|
||||
return True
|
||||
|
||||
|
@@ -245,7 +245,9 @@ class Remote:
|
||||
"""Return device info."""
|
||||
if self._control is None:
|
||||
return None
|
||||
return await self._handle_errors(self._control.get_device_info)
|
||||
device_info = await self._handle_errors(self._control.get_device_info)
|
||||
_LOGGER.debug("Fetched device info: %s", str(device_info))
|
||||
return device_info
|
||||
|
||||
async def _handle_errors(self, func, *args):
|
||||
"""Handle errors from func, set available and reconnect if needed."""
|
||||
|
@@ -18,4 +18,7 @@ ATTR_MANUFACTURER = "manufacturer"
|
||||
ATTR_MODEL_NUMBER = "modelNumber"
|
||||
ATTR_UDN = "UDN"
|
||||
|
||||
DEFAULT_MANUFACTURER = "Panasonic"
|
||||
DEFAULT_MODEL_NUMBER = "Panasonic Viera"
|
||||
|
||||
ERROR_INVALID_PIN_CODE = "invalid_pin_code"
|
||||
|
@@ -26,6 +26,8 @@ from .const import (
|
||||
ATTR_MODEL_NUMBER,
|
||||
ATTR_REMOTE,
|
||||
ATTR_UDN,
|
||||
DEFAULT_MANUFACTURER,
|
||||
DEFAULT_MODEL_NUMBER,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
@@ -69,11 +71,11 @@ class PanasonicVieraTVEntity(MediaPlayerEntity):
|
||||
self._device_info = device_info
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the device."""
|
||||
if self._device_info is not None:
|
||||
return self._device_info[ATTR_UDN]
|
||||
return None
|
||||
if self._device_info is None:
|
||||
return None
|
||||
return self._device_info[ATTR_UDN]
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
@@ -83,8 +85,10 @@ class PanasonicVieraTVEntity(MediaPlayerEntity):
|
||||
return {
|
||||
"name": self._name,
|
||||
"identifiers": {(DOMAIN, self._device_info[ATTR_UDN])},
|
||||
"manufacturer": self._device_info[ATTR_MANUFACTURER],
|
||||
"model": self._device_info[ATTR_MODEL_NUMBER],
|
||||
"manufacturer": self._device_info.get(
|
||||
ATTR_MANUFACTURER, DEFAULT_MANUFACTURER
|
||||
),
|
||||
"model": self._device_info.get(ATTR_MODEL_NUMBER, DEFAULT_MODEL_NUMBER),
|
||||
}
|
||||
|
||||
@property
|
||||
|
@@ -119,7 +119,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
if title is not None:
|
||||
try:
|
||||
title.hass = hass
|
||||
title = title.async_render()
|
||||
title = title.async_render(parse_result=False)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error("Error rendering title %s: %s", title, ex)
|
||||
title = title.template
|
||||
@@ -128,7 +128,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
|
||||
|
||||
try:
|
||||
message.hass = hass
|
||||
message = message.async_render()
|
||||
message = message.async_render(parse_result=False)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error("Error rendering message %s: %s", message, ex)
|
||||
message = message.template
|
||||
|
@@ -55,7 +55,7 @@ class Pi4ioe5v9Switch(SwitchEntity):
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._pin = pin
|
||||
self._invert_logic = invert_logic
|
||||
self._state = False
|
||||
self._state = not invert_logic
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@@ -84,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
|
||||
if resource_template is not None:
|
||||
resource_template.hass = hass
|
||||
resource = resource_template.render()
|
||||
resource = resource_template.render(parse_result=False)
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
@@ -189,6 +189,6 @@ class RestBinarySensor(BinarySensorEntity):
|
||||
async def async_update(self):
|
||||
"""Get the latest data from REST API and updates the state."""
|
||||
if self._resource_template is not None:
|
||||
self.rest.set_url(self._resource_template.render())
|
||||
self.rest.set_url(self._resource_template.render(parse_result=False))
|
||||
|
||||
await self.rest.async_update()
|
||||
|
@@ -163,7 +163,7 @@ class RestNotificationService(BaseNotificationService):
|
||||
key: _data_template_creator(item) for key, item in value.items()
|
||||
}
|
||||
value.hass = self._hass
|
||||
return value.async_render(kwargs)
|
||||
return value.async_render(kwargs, parse_result=False)
|
||||
|
||||
data.update(_data_template_creator(self._data_template))
|
||||
|
||||
|
@@ -103,7 +103,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
|
||||
if resource_template is not None:
|
||||
resource_template.hass = hass
|
||||
resource = resource_template.render()
|
||||
resource = resource_template.render(parse_result=False)
|
||||
|
||||
if username and password:
|
||||
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
|
||||
@@ -202,7 +202,7 @@ class RestSensor(Entity):
|
||||
async def async_update(self):
|
||||
"""Get the latest data from REST API and update the state."""
|
||||
if self._resource_template is not None:
|
||||
self.rest.set_url(self._resource_template.render())
|
||||
self.rest.set_url(self._resource_template.render(parse_result=False))
|
||||
|
||||
await self.rest.async_update()
|
||||
|
||||
|
@@ -162,7 +162,7 @@ class RestSwitch(SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
body_on_t = self._body_on.async_render()
|
||||
body_on_t = self._body_on.async_render(parse_result=False)
|
||||
|
||||
try:
|
||||
req = await self.set_device_state(body_on_t)
|
||||
@@ -178,7 +178,7 @@ class RestSwitch(SwitchEntity):
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
body_off_t = self._body_off.async_render()
|
||||
body_off_t = self._body_off.async_render(parse_result=False)
|
||||
|
||||
try:
|
||||
req = await self.set_device_state(body_off_t)
|
||||
|
@@ -93,17 +93,22 @@ async def async_setup(hass, config):
|
||||
payload = None
|
||||
if template_payload:
|
||||
payload = bytes(
|
||||
template_payload.async_render(variables=service.data), "utf-8"
|
||||
template_payload.async_render(
|
||||
variables=service.data, parse_result=False
|
||||
),
|
||||
"utf-8",
|
||||
)
|
||||
|
||||
request_url = template_url.async_render(variables=service.data)
|
||||
request_url = template_url.async_render(
|
||||
variables=service.data, parse_result=False
|
||||
)
|
||||
|
||||
headers = None
|
||||
if template_headers:
|
||||
headers = {}
|
||||
for header_name, template_header in template_headers.items():
|
||||
headers[header_name] = template_header.async_render(
|
||||
variables=service.data
|
||||
variables=service.data, parse_result=False
|
||||
)
|
||||
|
||||
if content_type:
|
||||
|
@@ -263,10 +263,10 @@ class RMVDepartureData:
|
||||
if not dest_found:
|
||||
continue
|
||||
|
||||
elif self._lines and journey["number"] not in self._lines:
|
||||
if self._lines and journey["number"] not in self._lines:
|
||||
continue
|
||||
|
||||
elif journey["minutes"] < self._time_offset:
|
||||
if journey["minutes"] < self._time_offset:
|
||||
continue
|
||||
|
||||
for attr in ["direction", "departure_time", "product", "minutes"]:
|
||||
|
@@ -2,6 +2,6 @@
|
||||
"domain": "rpi_gpio_pwm",
|
||||
"name": "pigpio Daemon PWM LED",
|
||||
"documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm",
|
||||
"requirements": ["pwmled==1.5.3"],
|
||||
"requirements": ["pwmled==1.6.6"],
|
||||
"codeowners": []
|
||||
}
|
||||
|
@@ -83,17 +83,19 @@ class RssView(HomeAssistantView):
|
||||
|
||||
response += "<rss>\n"
|
||||
if self._title is not None:
|
||||
response += " <title>%s</title>\n" % escape(self._title.async_render())
|
||||
response += " <title>%s</title>\n" % escape(
|
||||
self._title.async_render(parse_result=False)
|
||||
)
|
||||
|
||||
for item in self._items:
|
||||
response += " <item>\n"
|
||||
if "title" in item:
|
||||
response += " <title>"
|
||||
response += escape(item["title"].async_render())
|
||||
response += escape(item["title"].async_render(parse_result=False))
|
||||
response += "</title>\n"
|
||||
if "description" in item:
|
||||
response += " <description>"
|
||||
response += escape(item["description"].async_render())
|
||||
response += escape(item["description"].async_render(parse_result=False))
|
||||
response += "</description>\n"
|
||||
response += " </item>\n"
|
||||
|
||||
|
@@ -45,7 +45,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||
|
||||
if args_compiled:
|
||||
try:
|
||||
rendered_args = args_compiled.async_render(service.data)
|
||||
rendered_args = args_compiled.async_render(
|
||||
variables=service.data, parse_result=False
|
||||
)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.exception("Error rendering command template: %s", ex)
|
||||
return
|
||||
|
@@ -3,6 +3,6 @@
|
||||
"name": "SimpliSafe",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
|
||||
"requirements": ["simplisafe-python==9.5.1"],
|
||||
"requirements": ["simplisafe-python==9.6.0"],
|
||||
"codeowners": ["@bachya"]
|
||||
}
|
||||
|
@@ -118,7 +118,7 @@ def _async_templatize_blocks(hass, value):
|
||||
}
|
||||
|
||||
tmpl = template.Template(value, hass=hass)
|
||||
return tmpl.async_render()
|
||||
return tmpl.async_render(parse_result=False)
|
||||
|
||||
|
||||
class SlackNotificationService(BaseNotificationService):
|
||||
|
@@ -98,7 +98,7 @@ async def async_setup(hass, config):
|
||||
for site_id in site_ids:
|
||||
payload = json.dumps({"siteId": site_id})
|
||||
hass.components.mqtt.async_publish(
|
||||
FEEDBACK_ON_TOPIC, None, qos=0, retain=False
|
||||
FEEDBACK_ON_TOPIC, "", qos=0, retain=False
|
||||
)
|
||||
hass.components.mqtt.async_publish(
|
||||
topic, payload, qos=int(state), retain=state
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"domain": "spotify",
|
||||
"name": "Spotify",
|
||||
"documentation": "https://www.home-assistant.io/integrations/spotify",
|
||||
"requirements": ["spotipy==2.16.0"],
|
||||
"requirements": ["spotipy==2.16.1"],
|
||||
"zeroconf": ["_spotify-connect._tcp.local."],
|
||||
"dependencies": ["http"],
|
||||
"codeowners": ["@frenck"],
|
||||
|
@@ -65,7 +65,7 @@ CONTENT_TYPE_TO_CHILD_TYPE = {
|
||||
"Genres": MEDIA_TYPE_GENRE,
|
||||
}
|
||||
|
||||
BROWSE_LIMIT = 500
|
||||
BROWSE_LIMIT = 1000
|
||||
|
||||
|
||||
async def build_item_response(player, payload):
|
||||
|
@@ -6,7 +6,7 @@
|
||||
"@rajlaud"
|
||||
],
|
||||
"requirements": [
|
||||
"pysqueezebox==0.5.1"
|
||||
"pysqueezebox==0.5.4"
|
||||
],
|
||||
"config_flow": true
|
||||
}
|
||||
|
@@ -20,3 +20,6 @@ MIN_SEGMENT_DURATION = 1.5 # Each segment is at least this many seconds
|
||||
|
||||
PACKETS_TO_WAIT_FOR_AUDIO = 20 # Some streams have an audio stream with no audio
|
||||
MAX_TIMESTAMP_GAP = 10000 # seconds - anything from 10 to 50000 is probably reasonable
|
||||
|
||||
MAX_MISSING_DTS = 6 # Number of packets missing DTS to allow
|
||||
STREAM_TIMEOUT = 30 # Timeout for reading stream
|
||||
|
@@ -6,7 +6,13 @@ import time
|
||||
|
||||
import av
|
||||
|
||||
from .const import MAX_TIMESTAMP_GAP, MIN_SEGMENT_DURATION, PACKETS_TO_WAIT_FOR_AUDIO
|
||||
from .const import (
|
||||
MAX_MISSING_DTS,
|
||||
MAX_TIMESTAMP_GAP,
|
||||
MIN_SEGMENT_DURATION,
|
||||
PACKETS_TO_WAIT_FOR_AUDIO,
|
||||
STREAM_TIMEOUT,
|
||||
)
|
||||
from .core import Segment, StreamBuffer
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -62,7 +68,7 @@ def stream_worker(hass, stream, quit_event):
|
||||
def _stream_worker_internal(hass, stream, quit_event):
|
||||
"""Handle consuming streams."""
|
||||
|
||||
container = av.open(stream.source, options=stream.options)
|
||||
container = av.open(stream.source, options=stream.options, timeout=STREAM_TIMEOUT)
|
||||
try:
|
||||
video_stream = container.streams.video[0]
|
||||
except (KeyError, IndexError):
|
||||
@@ -81,13 +87,15 @@ def _stream_worker_internal(hass, stream, quit_event):
|
||||
if audio_stream and audio_stream.profile is None:
|
||||
audio_stream = None
|
||||
|
||||
# Iterator for demuxing
|
||||
container_packets = 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
|
||||
first_pts = {}
|
||||
# The decoder timestamps of the latest packet in each stream we processed
|
||||
last_dts = None
|
||||
# Keep track of consecutive packets without a dts to detect end of stream.
|
||||
last_packet_was_without_dts = False
|
||||
missing_dts = 0
|
||||
# Holds the buffers for each stream provider
|
||||
outputs = None
|
||||
# Keep track of the number of segments we've processed
|
||||
@@ -102,8 +110,8 @@ def _stream_worker_internal(hass, stream, quit_event):
|
||||
# 2 - seeking can be problematic https://trac.ffmpeg.org/ticket/7815
|
||||
|
||||
def peek_first_pts():
|
||||
nonlocal first_pts, audio_stream
|
||||
missing_dts = False
|
||||
nonlocal first_pts, audio_stream, container_packets
|
||||
missing_dts = 0
|
||||
|
||||
def empty_stream_dict():
|
||||
return {
|
||||
@@ -112,17 +120,20 @@ def _stream_worker_internal(hass, stream, quit_event):
|
||||
}
|
||||
|
||||
try:
|
||||
container_packets = container.demux((video_stream, audio_stream))
|
||||
first_packet = empty_stream_dict()
|
||||
first_pts = empty_stream_dict()
|
||||
# Get to first video keyframe
|
||||
while first_packet[video_stream] is None:
|
||||
packet = next(container.demux())
|
||||
packet = next(container_packets)
|
||||
if (
|
||||
packet.dts is None
|
||||
): # Allow single packet with no dts, raise error on second
|
||||
if missing_dts:
|
||||
raise av.AVError
|
||||
missing_dts = True
|
||||
): # Allow MAX_MISSING_DTS packets with no dts, raise error on the next one
|
||||
if missing_dts >= MAX_MISSING_DTS:
|
||||
raise StopIteration(
|
||||
f"Invalid data - got {MAX_MISSING_DTS+1} packets with missing DTS while initializing"
|
||||
)
|
||||
missing_dts += 1
|
||||
continue
|
||||
if packet.stream == video_stream and packet.is_keyframe:
|
||||
first_packet[video_stream] = packet
|
||||
@@ -131,13 +142,15 @@ def _stream_worker_internal(hass, stream, quit_event):
|
||||
while any(
|
||||
[pts is None for pts in {**first_packet, **first_pts}.values()]
|
||||
) and (len(initial_packets) < PACKETS_TO_WAIT_FOR_AUDIO):
|
||||
packet = next(container.demux((video_stream, audio_stream)))
|
||||
packet = next(container_packets)
|
||||
if (
|
||||
packet.dts is None
|
||||
): # Allow single packet with no dts, raise error on second
|
||||
if missing_dts:
|
||||
raise av.AVError
|
||||
missing_dts = True
|
||||
): # Allow MAX_MISSING_DTS packet with no dts, raise error on the next one
|
||||
if missing_dts >= MAX_MISSING_DTS:
|
||||
raise StopIteration(
|
||||
f"Invalid data - got {MAX_MISSING_DTS+1} packets with missing DTS while initializing"
|
||||
)
|
||||
missing_dts += 1
|
||||
continue
|
||||
if (
|
||||
first_packet[packet.stream] is None
|
||||
@@ -223,16 +236,16 @@ def _stream_worker_internal(hass, stream, quit_event):
|
||||
if len(initial_packets) > 0:
|
||||
packet = initial_packets.popleft()
|
||||
else:
|
||||
packet = next(container.demux((video_stream, audio_stream)))
|
||||
packet = next(container_packets)
|
||||
if packet.dts is None:
|
||||
_LOGGER.error("Stream packet without dts detected, skipping...")
|
||||
# Allow a single packet without dts before terminating the stream.
|
||||
if last_packet_was_without_dts:
|
||||
# If we get a "flushing" packet, the stream is done
|
||||
raise StopIteration("No dts in consecutive packets")
|
||||
last_packet_was_without_dts = True
|
||||
# Allow MAX_MISSING_DTS consecutive packets without dts. Terminate the stream on the next one.
|
||||
if missing_dts >= MAX_MISSING_DTS:
|
||||
raise StopIteration(
|
||||
f"No dts in {MAX_MISSING_DTS+1} consecutive packets"
|
||||
)
|
||||
missing_dts += 1
|
||||
continue
|
||||
last_packet_was_without_dts = False
|
||||
missing_dts = 0
|
||||
except (av.AVError, StopIteration) as ex:
|
||||
_LOGGER.error("Error demuxing stream: %s", str(ex))
|
||||
finalize_stream()
|
||||
|
@@ -316,6 +316,8 @@ class SynoApi:
|
||||
)
|
||||
self._with_surveillance_station = bool(
|
||||
self._fetching_entities.get(SynoSurveillanceStation.CAMERA_API_KEY)
|
||||
) or bool(
|
||||
self._fetching_entities.get(SynoSurveillanceStation.HOME_MODE_API_KEY)
|
||||
)
|
||||
|
||||
# Reset not used API, information is not reset since it's used in device_info
|
||||
|
@@ -275,7 +275,6 @@ def _login_and_fetch_syno_info(api, otp_code):
|
||||
if (
|
||||
not api.information.serial
|
||||
or api.utilisation.cpu_user_load is None
|
||||
or not api.storage.disks_ids
|
||||
or not api.storage.volumes_ids
|
||||
or not api.network.macs
|
||||
):
|
||||
|
@@ -19,12 +19,10 @@ from homeassistant.components.mqtt.subscription import (
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from . import device_automation, discovery
|
||||
from .const import CONF_DISCOVERY_PREFIX, DATA_REMOVE_DISCOVER_COMPONENT, PLATFORMS
|
||||
from .discovery import TASMOTA_DISCOVERY_DEVICE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,13 +53,11 @@ async def async_setup_entry(hass, entry):
|
||||
|
||||
tasmota_mqtt = TasmotaMQTTClient(_publish, _subscribe_topics, _unsubscribe_topics)
|
||||
|
||||
async def async_discover_device(config, mac):
|
||||
"""Discover and add a Tasmota device."""
|
||||
await async_setup_device(hass, mac, config, entry, tasmota_mqtt)
|
||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||
|
||||
hass.data[
|
||||
DATA_REMOVE_DISCOVER_COMPONENT.format("device")
|
||||
] = async_dispatcher_connect(hass, TASMOTA_DISCOVERY_DEVICE, async_discover_device)
|
||||
def async_discover_device(config, mac):
|
||||
"""Discover and add a Tasmota device."""
|
||||
async_setup_device(hass, mac, config, entry, tasmota_mqtt, device_registry)
|
||||
|
||||
async def start_platforms():
|
||||
await device_automation.async_setup_entry(hass, entry)
|
||||
@@ -73,7 +69,9 @@ async def async_setup_entry(hass, entry):
|
||||
)
|
||||
|
||||
discovery_prefix = entry.data[CONF_DISCOVERY_PREFIX]
|
||||
await discovery.async_start(hass, discovery_prefix, entry, tasmota_mqtt)
|
||||
await discovery.async_start(
|
||||
hass, discovery_prefix, entry, tasmota_mqtt, async_discover_device
|
||||
)
|
||||
|
||||
hass.async_create_task(start_platforms())
|
||||
return True
|
||||
@@ -97,7 +95,6 @@ async def async_unload_entry(hass, entry):
|
||||
# disable discovery
|
||||
await discovery.async_stop(hass)
|
||||
hass.data.pop(DEVICE_MACS)
|
||||
hass.data[DATA_REMOVE_DISCOVER_COMPONENT.format("device")]()
|
||||
hass.data.pop(DATA_REMOVE_DISCOVER_COMPONENT.format("device_automation"))()
|
||||
for component in PLATFORMS:
|
||||
hass.data.pop(DATA_REMOVE_DISCOVER_COMPONENT.format(component))()
|
||||
@@ -105,9 +102,8 @@ async def async_unload_entry(hass, entry):
|
||||
return True
|
||||
|
||||
|
||||
async def _remove_device(hass, config_entry, mac, tasmota_mqtt):
|
||||
def _remove_device(hass, config_entry, mac, tasmota_mqtt, device_registry):
|
||||
"""Remove device from device registry."""
|
||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||
device = device_registry.async_get_device(set(), {(CONNECTION_NETWORK_MAC, mac)})
|
||||
|
||||
if device is None:
|
||||
@@ -118,9 +114,8 @@ async def _remove_device(hass, config_entry, mac, tasmota_mqtt):
|
||||
clear_discovery_topic(mac, config_entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt)
|
||||
|
||||
|
||||
async def _update_device(hass, config_entry, config):
|
||||
def _update_device(hass, config_entry, config, device_registry):
|
||||
"""Add or update device registry."""
|
||||
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||
config_entry_id = config_entry.entry_id
|
||||
device_info = {
|
||||
"connections": {(CONNECTION_NETWORK_MAC, config[CONF_MAC])},
|
||||
@@ -135,9 +130,9 @@ async def _update_device(hass, config_entry, config):
|
||||
hass.data[DEVICE_MACS][device.id] = config[CONF_MAC]
|
||||
|
||||
|
||||
async def async_setup_device(hass, mac, config, config_entry, tasmota_mqtt):
|
||||
def async_setup_device(hass, mac, config, config_entry, tasmota_mqtt, device_registry):
|
||||
"""Set up the Tasmota device."""
|
||||
if not config:
|
||||
await _remove_device(hass, config_entry, mac, tasmota_mqtt)
|
||||
_remove_device(hass, config_entry, mac, tasmota_mqtt, device_registry)
|
||||
else:
|
||||
await _update_device(hass, config_entry, config)
|
||||
_update_device(hass, config_entry, config, device_registry)
|
||||
|
@@ -21,7 +21,6 @@ from .const import DOMAIN, PLATFORMS
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ALREADY_DISCOVERED = "tasmota_discovered_components"
|
||||
TASMOTA_DISCOVERY_DEVICE = "tasmota_discovery_device"
|
||||
TASMOTA_DISCOVERY_ENTITY_NEW = "tasmota_discovery_entity_new_{}"
|
||||
TASMOTA_DISCOVERY_ENTITY_UPDATED = "tasmota_discovery_entity_updated_{}_{}_{}_{}"
|
||||
TASMOTA_DISCOVERY_INSTANCE = "tasmota_discovery_instance"
|
||||
@@ -29,6 +28,9 @@ TASMOTA_DISCOVERY_INSTANCE = "tasmota_discovery_instance"
|
||||
|
||||
def clear_discovery_hash(hass, discovery_hash):
|
||||
"""Clear entry in ALREADY_DISCOVERED list."""
|
||||
if ALREADY_DISCOVERED not in hass.data:
|
||||
# Discovery is shutting down
|
||||
return
|
||||
del hass.data[ALREADY_DISCOVERED][discovery_hash]
|
||||
|
||||
|
||||
@@ -38,7 +40,7 @@ def set_discovery_hash(hass, discovery_hash):
|
||||
|
||||
|
||||
async def async_start(
|
||||
hass: HomeAssistantType, discovery_topic, config_entry, tasmota_mqtt
|
||||
hass: HomeAssistantType, discovery_topic, config_entry, tasmota_mqtt, setup_device
|
||||
) -> bool:
|
||||
"""Start Tasmota device discovery."""
|
||||
|
||||
@@ -92,9 +94,7 @@ async def async_start(
|
||||
|
||||
_LOGGER.debug("Received discovery data for tasmota device: %s", mac)
|
||||
tasmota_device_config = tasmota_get_device_config(payload)
|
||||
async_dispatcher_send(
|
||||
hass, TASMOTA_DISCOVERY_DEVICE, tasmota_device_config, mac
|
||||
)
|
||||
setup_device(tasmota_device_config, mac)
|
||||
|
||||
if not payload:
|
||||
return
|
||||
|
@@ -80,10 +80,11 @@ class TasmotaLight(
|
||||
|
||||
self._setup_from_entity()
|
||||
|
||||
async def discovery_update(self, update):
|
||||
async def discovery_update(self, update, write_state=True):
|
||||
"""Handle updated discovery message."""
|
||||
await super().discovery_update(update, write_state=False)
|
||||
self._setup_from_entity()
|
||||
await super().discovery_update(update)
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _setup_from_entity(self):
|
||||
"""(Re)Setup the entity."""
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"name": "Tasmota (beta)",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/tasmota",
|
||||
"requirements": ["hatasmota==0.0.20"],
|
||||
"requirements": ["hatasmota==0.0.25"],
|
||||
"dependencies": ["mqtt"],
|
||||
"mqtt": ["tasmota/discovery/#"],
|
||||
"codeowners": ["@emontnemery"]
|
||||
|
@@ -38,11 +38,12 @@ class TasmotaEntity(Entity):
|
||||
await self._tasmota_entity.unsubscribe_topics()
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
async def discovery_update(self, update):
|
||||
async def discovery_update(self, update, write_state=True):
|
||||
"""Handle updated discovery message."""
|
||||
self._tasmota_entity.config_update(update)
|
||||
await self._subscribe_topics()
|
||||
self.async_write_ha_state()
|
||||
if write_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
|
@@ -1,6 +1,7 @@
|
||||
"""Support for Tasmota sensors."""
|
||||
from typing import Optional
|
||||
|
||||
from hatasmota import status_sensor
|
||||
from hatasmota.const import (
|
||||
SENSOR_AMBIENT,
|
||||
SENSOR_APPARENT_POWERUSAGE,
|
||||
@@ -162,7 +163,7 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, Entity):
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
# Hide status sensors to not overwhelm users
|
||||
if self._tasmota_entity.quantity == SENSOR_STATUS_SIGNAL:
|
||||
if self._tasmota_entity.quantity in status_sensor.SENSORS:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
@@ -136,7 +136,9 @@ class TcpSensor(Entity):
|
||||
|
||||
if self._config[CONF_VALUE_TEMPLATE] is not None:
|
||||
try:
|
||||
self._state = self._config[CONF_VALUE_TEMPLATE].render(value=value)
|
||||
self._state = self._config[CONF_VALUE_TEMPLATE].render(
|
||||
parse_result=False, value=value
|
||||
)
|
||||
return
|
||||
except TemplateError:
|
||||
_LOGGER.error(
|
||||
|
@@ -329,7 +329,9 @@ async def async_setup(hass, config):
|
||||
else:
|
||||
attribute_templ.hass = hass
|
||||
try:
|
||||
data[attribute] = attribute_templ.async_render()
|
||||
data[attribute] = attribute_templ.async_render(
|
||||
parse_result=False
|
||||
)
|
||||
except TemplateError as exc:
|
||||
_LOGGER.error(
|
||||
"TemplateError in %s: %s -> %s",
|
||||
|
@@ -259,6 +259,7 @@ class CoverTemplate(TemplateEntity, CoverEntity):
|
||||
return
|
||||
|
||||
state = str(result).lower()
|
||||
|
||||
if state in _VALID_STATES:
|
||||
if state in ("true", STATE_OPEN):
|
||||
self._position = 100
|
||||
|
@@ -368,6 +368,7 @@ class TemplateFan(TemplateEntity, FanEntity):
|
||||
def _update_speed(self, speed):
|
||||
# Validate speed
|
||||
speed = str(speed)
|
||||
|
||||
if speed in self._speed_list:
|
||||
self._speed = speed
|
||||
elif speed in [STATE_UNAVAILABLE, STATE_UNKNOWN]:
|
||||
|
@@ -412,16 +412,21 @@ class LightTemplate(TemplateEntity, LightEntity):
|
||||
self._available = True
|
||||
return
|
||||
|
||||
if isinstance(result, bool):
|
||||
self._state = result
|
||||
return
|
||||
|
||||
state = str(result).lower()
|
||||
if state in _VALID_STATES:
|
||||
self._state = state in ("true", STATE_ON)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Received invalid light is_on state: %s. Expected: %s",
|
||||
state,
|
||||
", ".join(_VALID_STATES),
|
||||
)
|
||||
self._state = None
|
||||
return
|
||||
|
||||
_LOGGER.error(
|
||||
"Received invalid light is_on state: %s. Expected: %s",
|
||||
state,
|
||||
", ".join(_VALID_STATES),
|
||||
)
|
||||
self._state = None
|
||||
|
||||
@callback
|
||||
def _update_temperature(self, render):
|
||||
|
@@ -282,7 +282,7 @@ class TeslaDevice(CoordinatorEntity):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the device."""
|
||||
attr = self._attributes.copy()
|
||||
attr = self._attributes
|
||||
if self.tesla_device.has_battery():
|
||||
attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level()
|
||||
attr[ATTR_BATTERY_CHARGING] = self.tesla_device.battery_charging()
|
||||
@@ -310,4 +310,5 @@ class TeslaDevice(CoordinatorEntity):
|
||||
This assumes the coordinator has updated the controller.
|
||||
"""
|
||||
self.tesla_device.refresh()
|
||||
self._attributes = self.tesla_device.attrs.copy()
|
||||
self.async_write_ha_state()
|
||||
|
@@ -2,7 +2,6 @@
|
||||
import logging
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.const import STATE_ON
|
||||
|
||||
from . import DOMAIN as TESLA_DOMAIN, TeslaDevice
|
||||
|
||||
@@ -42,7 +41,7 @@ class ChargerSwitch(TeslaDevice, SwitchEntity):
|
||||
"""Get whether the switch is in on state."""
|
||||
if self.tesla_device.is_charging() is None:
|
||||
return None
|
||||
return self.tesla_device.is_charging() == STATE_ON
|
||||
return self.tesla_device.is_charging()
|
||||
|
||||
|
||||
class RangeSwitch(TeslaDevice, SwitchEntity):
|
||||
|
@@ -13,6 +13,8 @@ from pyHS100 import (
|
||||
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from .const import DOMAIN as TPLINK_DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -127,3 +129,29 @@ def get_static_devices(config_data) -> SmartDevices:
|
||||
"Failed to setup device %s due to %s; not retrying", host, sde
|
||||
)
|
||||
return SmartDevices(lights, switches)
|
||||
|
||||
|
||||
def add_available_devices(hass, device_type, device_class):
|
||||
"""Get sysinfo for all devices."""
|
||||
|
||||
devices = hass.data[TPLINK_DOMAIN][device_type]
|
||||
|
||||
if f"{device_type}_remaining" in hass.data[TPLINK_DOMAIN]:
|
||||
devices = hass.data[TPLINK_DOMAIN][f"{device_type}_remaining"]
|
||||
|
||||
entities_ready = []
|
||||
devices_unavailable = []
|
||||
for device in devices:
|
||||
try:
|
||||
device.get_sysinfo()
|
||||
entities_ready.append(device_class(device))
|
||||
except SmartDeviceException as ex:
|
||||
devices_unavailable.append(device)
|
||||
_LOGGER.warning(
|
||||
"Unable to communicate with device %s: %s",
|
||||
device.host,
|
||||
ex,
|
||||
)
|
||||
|
||||
hass.data[TPLINK_DOMAIN][f"{device_type}_remaining"] = devices_unavailable
|
||||
return entities_ready
|
||||
|
@@ -16,7 +16,7 @@ from homeassistant.components.light import (
|
||||
SUPPORT_COLOR_TEMP,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
from homeassistant.util.color import (
|
||||
@@ -26,6 +26,7 @@ from homeassistant.util.color import (
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import CONF_LIGHT, DOMAIN as TPLINK_DOMAIN
|
||||
from .common import add_available_devices
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
@@ -60,20 +61,15 @@ SLEEP_TIME = 2
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities):
|
||||
"""Set up lights."""
|
||||
devices = hass.data[TPLINK_DOMAIN][CONF_LIGHT]
|
||||
entities = []
|
||||
entities = await hass.async_add_executor_job(
|
||||
add_available_devices, hass, CONF_LIGHT, TPLinkSmartBulb
|
||||
)
|
||||
|
||||
await hass.async_add_executor_job(get_devices_sysinfo, devices)
|
||||
for device in devices:
|
||||
entities.append(TPLinkSmartBulb(device))
|
||||
if entities:
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
||||
def get_devices_sysinfo(devices):
|
||||
"""Get sysinfo for all devices."""
|
||||
for device in devices:
|
||||
device.get_sysinfo()
|
||||
if hass.data[TPLINK_DOMAIN][f"{CONF_LIGHT}_remaining"]:
|
||||
raise PlatformNotReady
|
||||
|
||||
|
||||
def brightness_to_percentage(byt):
|
||||
|
@@ -11,10 +11,12 @@ from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_VOLTAGE
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from . import CONF_SWITCH, DOMAIN as TPLINK_DOMAIN
|
||||
from .common import add_available_devices
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -29,20 +31,15 @@ SLEEP_TIME = 2
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities):
|
||||
"""Set up switches."""
|
||||
devices = hass.data[TPLINK_DOMAIN][CONF_SWITCH]
|
||||
entities = []
|
||||
entities = await hass.async_add_executor_job(
|
||||
add_available_devices, hass, CONF_SWITCH, SmartPlugSwitch
|
||||
)
|
||||
|
||||
await hass.async_add_executor_job(get_devices_sysinfo, devices)
|
||||
for device in devices:
|
||||
entities.append(SmartPlugSwitch(device))
|
||||
if entities:
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
||||
def get_devices_sysinfo(devices):
|
||||
"""Get sysinfo for all devices."""
|
||||
for device in devices:
|
||||
device.get_sysinfo()
|
||||
if hass.data[TPLINK_DOMAIN][f"{CONF_SWITCH}_remaining"]:
|
||||
raise PlatformNotReady
|
||||
|
||||
|
||||
class SmartPlugSwitch(SwitchEntity):
|
||||
|
@@ -21,6 +21,7 @@ from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import state as state_helper
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@@ -165,6 +166,7 @@ class WatsonIOTThread(threading.Thread):
|
||||
self.shutdown = False
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener)
|
||||
|
||||
@callback
|
||||
def _event_listener(self, event):
|
||||
"""Listen for new messages on the bus and queue them for Watson IoT."""
|
||||
item = (time.monotonic(), event)
|
||||
|
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"title": "xbox",
|
||||
"config": {
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
|
@@ -13,6 +13,5 @@
|
||||
"title": "Pick Authentication Method"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "xbox"
|
||||
}
|
||||
}
|
@@ -142,7 +142,7 @@ class XiaomiCamera(Camera):
|
||||
"""Return a still image response from the camera."""
|
||||
|
||||
try:
|
||||
host = self.host.async_render()
|
||||
host = self.host.async_render(parse_result=False)
|
||||
except TemplateError as exc:
|
||||
_LOGGER.error("Error parsing template %s: %s", self.host, exc)
|
||||
return self._last_image
|
||||
|
@@ -195,8 +195,9 @@ class AirMonitorS1(AirMonitorB1):
|
||||
self._humidity = state.humidity
|
||||
self._available = True
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
|
||||
class AirMonitorV1(AirMonitorB1):
|
||||
@@ -210,8 +211,9 @@ class AirMonitorV1(AirMonitorB1):
|
||||
self._air_quality_index = state.aqi
|
||||
self._available = True
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
|
@@ -3,7 +3,7 @@
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from miio.gateway import GatewayException
|
||||
from miio import DeviceException
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
SUPPORT_ALARM_ARM_AWAY,
|
||||
@@ -103,7 +103,7 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity):
|
||||
partial(func, *args, **kwargs)
|
||||
)
|
||||
_LOGGER.debug("Response received from miio device: %s", result)
|
||||
except GatewayException as exc:
|
||||
except DeviceException as exc:
|
||||
_LOGGER.error(mask_error, exc)
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
@@ -122,9 +122,11 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity):
|
||||
"""Fetch state from the device."""
|
||||
try:
|
||||
state = await self.hass.async_add_executor_job(self._gateway.alarm.status)
|
||||
except GatewayException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
except DeviceException as ex:
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
return
|
||||
|
||||
_LOGGER.debug("Got new state: %s", state)
|
||||
|
@@ -655,8 +655,10 @@ class XiaomiGenericDevice(FanEntity):
|
||||
|
||||
return result == SUCCESS
|
||||
except DeviceException as exc:
|
||||
_LOGGER.error(mask_error, exc)
|
||||
self._available = False
|
||||
if self._available:
|
||||
_LOGGER.error(mask_error, exc)
|
||||
self._available = False
|
||||
|
||||
return False
|
||||
|
||||
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
|
||||
@@ -785,8 +787,9 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
|
||||
)
|
||||
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
@@ -1029,8 +1032,9 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
|
||||
)
|
||||
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
@@ -1138,8 +1142,9 @@ class XiaomiAirFresh(XiaomiGenericDevice):
|
||||
)
|
||||
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
|
@@ -65,7 +65,7 @@ class XiaomiGatewayDevice(Entity):
|
||||
self._entry = entry
|
||||
self._unique_id = sub_device.sid
|
||||
self._name = f"{sub_device.name} ({sub_device.sid})"
|
||||
self._available = None
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -100,5 +100,6 @@ class XiaomiGatewayDevice(Entity):
|
||||
await self.hass.async_add_executor_job(self._sub_device.update)
|
||||
self._available = True
|
||||
except gateway.GatewayException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
@@ -324,8 +324,10 @@ class XiaomiPhilipsAbstractLight(LightEntity):
|
||||
|
||||
return result == SUCCESS
|
||||
except DeviceException as exc:
|
||||
_LOGGER.error(mask_error, exc)
|
||||
self._available = False
|
||||
if self._available:
|
||||
_LOGGER.error(mask_error, exc)
|
||||
self._available = False
|
||||
|
||||
return False
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
@@ -356,8 +358,10 @@ class XiaomiPhilipsAbstractLight(LightEntity):
|
||||
try:
|
||||
state = await self.hass.async_add_executor_job(self._light.status)
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
return
|
||||
|
||||
_LOGGER.debug("Got new state: %s", state)
|
||||
@@ -380,8 +384,10 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight):
|
||||
try:
|
||||
state = await self.hass.async_add_executor_job(self._light.status)
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
return
|
||||
|
||||
_LOGGER.debug("Got new state: %s", state)
|
||||
@@ -536,8 +542,10 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight):
|
||||
try:
|
||||
state = await self.hass.async_add_executor_job(self._light.status)
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
return
|
||||
|
||||
_LOGGER.debug("Got new state: %s", state)
|
||||
@@ -593,8 +601,10 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb):
|
||||
try:
|
||||
state = await self.hass.async_add_executor_job(self._light.status)
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
return
|
||||
|
||||
_LOGGER.debug("Got new state: %s", state)
|
||||
@@ -637,8 +647,10 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight):
|
||||
try:
|
||||
state = await self.hass.async_add_executor_job(self._light.status)
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
return
|
||||
|
||||
_LOGGER.debug("Got new state: %s", state)
|
||||
@@ -778,8 +790,10 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight):
|
||||
try:
|
||||
state = await self.hass.async_add_executor_job(self._light.status)
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
return
|
||||
|
||||
_LOGGER.debug("Got new state: %s", state)
|
||||
@@ -932,8 +946,10 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb):
|
||||
try:
|
||||
state = await self.hass.async_add_executor_job(self._light.status)
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
return
|
||||
|
||||
_LOGGER.debug("Got new state: %s", state)
|
||||
|
@@ -233,8 +233,9 @@ class XiaomiAirQualityMonitor(Entity):
|
||||
)
|
||||
|
||||
except DeviceException as ex:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
if self._available:
|
||||
self._available = False
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
|
||||
class XiaomiGatewaySensor(XiaomiGatewayDevice):
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user