mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
Merge pull request #38443 from home-assistant/rc
This commit is contained in:
commit
c3aa9f9a6f
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@ -46,7 +46,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python -m venv venv
|
python -m venv venv
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pip install -U pip setuptools
|
pip install -U pip==20.1.1 setuptools
|
||||||
pip install -r requirements.txt -r requirements_test.txt
|
pip install -r requirements.txt -r requirements_test.txt
|
||||||
# Uninstalling typing as a workaround. Eventually we should make sure
|
# Uninstalling typing as a workaround. Eventually we should make sure
|
||||||
# all our dependencies drop typing.
|
# all our dependencies drop typing.
|
||||||
@ -603,7 +603,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python -m venv venv
|
python -m venv venv
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
pip install -U pip setuptools wheel
|
pip install -U pip==20.1.1 setuptools wheel
|
||||||
pip install -r requirements_all.txt
|
pip install -r requirements_all.txt
|
||||||
pip install -r requirements_test.txt
|
pip install -r requirements_test.txt
|
||||||
# Uninstalling typing as a workaround. Eventually we should make sure
|
# Uninstalling typing as a workaround. Eventually we should make sure
|
||||||
|
@ -261,6 +261,7 @@ def setup_abode_events(hass):
|
|||||||
TIMELINE.AUTOMATION_GROUP,
|
TIMELINE.AUTOMATION_GROUP,
|
||||||
TIMELINE.DISARM_GROUP,
|
TIMELINE.DISARM_GROUP,
|
||||||
TIMELINE.ARM_GROUP,
|
TIMELINE.ARM_GROUP,
|
||||||
|
TIMELINE.ARM_FAULT_GROUP,
|
||||||
TIMELINE.TEST_GROUP,
|
TIMELINE.TEST_GROUP,
|
||||||
TIMELINE.CAPTURE_GROUP,
|
TIMELINE.CAPTURE_GROUP,
|
||||||
TIMELINE.DEVICE_GROUP,
|
TIMELINE.DEVICE_GROUP,
|
||||||
|
@ -82,8 +82,21 @@ class AbodeCamera(AbodeDevice, Camera):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
"""Turn on camera."""
|
||||||
|
self._device.privacy_mode(False)
|
||||||
|
|
||||||
|
def turn_off(self):
|
||||||
|
"""Turn off camera."""
|
||||||
|
self._device.privacy_mode(True)
|
||||||
|
|
||||||
def _capture_callback(self, capture):
|
def _capture_callback(self, capture):
|
||||||
"""Update the image with the device then refresh device."""
|
"""Update the image with the device then refresh device."""
|
||||||
self._device.update_image_location(capture)
|
self._device.update_image_location(capture)
|
||||||
self.get_image()
|
self.get_image()
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if on."""
|
||||||
|
return self._device.is_on
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Abode",
|
"name": "Abode",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/abode",
|
"documentation": "https://www.home-assistant.io/integrations/abode",
|
||||||
"requirements": ["abodepy==0.19.0"],
|
"requirements": ["abodepy==1.1.0"],
|
||||||
"codeowners": ["@shred86"],
|
"codeowners": ["@shred86"],
|
||||||
"homekit": {
|
"homekit": {
|
||||||
"models": ["Abode", "Iota"]
|
"models": ["Abode", "Iota"]
|
||||||
|
@ -230,7 +230,13 @@ class AdsHub:
|
|||||||
|
|
||||||
hnotify = int(contents.hNotification)
|
hnotify = int(contents.hNotification)
|
||||||
_LOGGER.debug("Received notification %d", hnotify)
|
_LOGGER.debug("Received notification %d", hnotify)
|
||||||
data = contents.data
|
|
||||||
|
# get dynamically sized data array
|
||||||
|
data_size = contents.cbSampleSize
|
||||||
|
data = (ctypes.c_ubyte * data_size).from_address(
|
||||||
|
ctypes.addressof(contents)
|
||||||
|
+ pyads.structs.SAdsNotificationHeader.data.offset
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@ -241,17 +247,17 @@ class AdsHub:
|
|||||||
|
|
||||||
# Parse data to desired datatype
|
# Parse data to desired datatype
|
||||||
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
|
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
|
||||||
value = bool(struct.unpack("<?", bytearray(data)[:1])[0])
|
value = bool(struct.unpack("<?", bytearray(data))[0])
|
||||||
elif notification_item.plc_datatype == self.PLCTYPE_INT:
|
elif notification_item.plc_datatype == self.PLCTYPE_INT:
|
||||||
value = struct.unpack("<h", bytearray(data)[:2])[0]
|
value = struct.unpack("<h", bytearray(data))[0]
|
||||||
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
|
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
|
||||||
value = struct.unpack("<B", bytearray(data)[:1])[0]
|
value = struct.unpack("<B", bytearray(data))[0]
|
||||||
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
|
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
|
||||||
value = struct.unpack("<H", bytearray(data)[:2])[0]
|
value = struct.unpack("<H", bytearray(data))[0]
|
||||||
elif notification_item.plc_datatype == self.PLCTYPE_DINT:
|
elif notification_item.plc_datatype == self.PLCTYPE_DINT:
|
||||||
value = struct.unpack("<i", bytearray(data)[:4])[0]
|
value = struct.unpack("<i", bytearray(data))[0]
|
||||||
elif notification_item.plc_datatype == self.PLCTYPE_UDINT:
|
elif notification_item.plc_datatype == self.PLCTYPE_UDINT:
|
||||||
value = struct.unpack("<I", bytearray(data)[:4])[0]
|
value = struct.unpack("<I", bytearray(data))[0]
|
||||||
else:
|
else:
|
||||||
value = bytearray(data)
|
value = bytearray(data)
|
||||||
_LOGGER.warning("No callback available for this datatype")
|
_LOGGER.warning("No callback available for this datatype")
|
||||||
|
@ -2,6 +2,6 @@
|
|||||||
"domain": "ads",
|
"domain": "ads",
|
||||||
"name": "ADS",
|
"name": "ADS",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ads",
|
"documentation": "https://www.home-assistant.io/integrations/ads",
|
||||||
"requirements": ["pyads==3.1.3"],
|
"requirements": ["pyads==3.2.1"],
|
||||||
"codeowners": []
|
"codeowners": []
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
"name": "Android TV",
|
"name": "Android TV",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
"documentation": "https://www.home-assistant.io/integrations/androidtv",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"adb-shell[async]==0.2.0",
|
"adb-shell[async]==0.2.1",
|
||||||
"androidtv[async]==0.0.46",
|
"androidtv[async]==0.0.47",
|
||||||
"pure-python-adb==0.2.2.dev0"
|
"pure-python-adb==0.2.2.dev0"
|
||||||
],
|
],
|
||||||
"codeowners": ["@JeffLIrion"]
|
"codeowners": ["@JeffLIrion"]
|
||||||
|
@ -1,16 +1,25 @@
|
|||||||
"""Config flow for Cast."""
|
"""Config flow for Cast."""
|
||||||
from pychromecast.discovery import discover_chromecasts
|
import functools
|
||||||
|
|
||||||
|
from pychromecast.discovery import discover_chromecasts, stop_discovery
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.helpers import config_entry_flow
|
from homeassistant.helpers import config_entry_flow
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .helpers import ChromeCastZeroconf
|
||||||
|
|
||||||
|
|
||||||
async def _async_has_devices(hass):
|
async def _async_has_devices(hass):
|
||||||
"""Return if there are devices that can be discovered."""
|
"""Return if there are devices that can be discovered."""
|
||||||
|
|
||||||
return await hass.async_add_executor_job(discover_chromecasts)
|
casts, browser = await hass.async_add_executor_job(
|
||||||
|
functools.partial(
|
||||||
|
discover_chromecasts, zeroconf_instance=ChromeCastZeroconf.get_zeroconf()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stop_discovery(browser)
|
||||||
|
return casts
|
||||||
|
|
||||||
|
|
||||||
config_entry_flow.register_discovery_flow(
|
config_entry_flow.register_discovery_flow(
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Google Cast",
|
"name": "Google Cast",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||||
"requirements": ["pychromecast==7.1.2"],
|
"requirements": ["pychromecast==7.2.0"],
|
||||||
"after_dependencies": ["cloud","zeroconf"],
|
"after_dependencies": ["cloud","zeroconf"],
|
||||||
"zeroconf": ["_googlecast._tcp.local."],
|
"zeroconf": ["_googlecast._tcp.local."],
|
||||||
"codeowners": ["@emontnemery"]
|
"codeowners": ["@emontnemery"]
|
||||||
|
@ -8,7 +8,6 @@ from aiohttp.hdrs import REFERER, USER_AGENT
|
|||||||
import async_timeout
|
import async_timeout
|
||||||
from gtts_token import gtts_token
|
from gtts_token import gtts_token
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import yarl
|
|
||||||
|
|
||||||
from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider
|
from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider
|
||||||
from homeassistant.const import HTTP_OK
|
from homeassistant.const import HTTP_OK
|
||||||
@ -129,7 +128,7 @@ class GoogleProvider(Provider):
|
|||||||
url_param = {
|
url_param = {
|
||||||
"ie": "UTF-8",
|
"ie": "UTF-8",
|
||||||
"tl": language,
|
"tl": language,
|
||||||
"q": yarl.URL(part).raw_path,
|
"q": part,
|
||||||
"tk": part_token,
|
"tk": part_token,
|
||||||
"total": len(message_parts),
|
"total": len(message_parts),
|
||||||
"idx": idx,
|
"idx": idx,
|
||||||
|
@ -164,6 +164,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
def _host_already_configured(self, host):
|
def _host_already_configured(self, host):
|
||||||
"""See if we already have a harmony entry matching the host."""
|
"""See if we already have a harmony entry matching the host."""
|
||||||
for entry in self._async_current_entries():
|
for entry in self._async_current_entries():
|
||||||
|
if CONF_HOST not in entry.data:
|
||||||
|
continue
|
||||||
|
|
||||||
if entry.data[CONF_HOST] == host:
|
if entry.data[CONF_HOST] == host:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "harmony",
|
"domain": "harmony",
|
||||||
"name": "Logitech Harmony Hub",
|
"name": "Logitech Harmony Hub",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/harmony",
|
"documentation": "https://www.home-assistant.io/integrations/harmony",
|
||||||
"requirements": ["aioharmony==0.2.5"],
|
"requirements": ["aioharmony==0.2.6"],
|
||||||
"codeowners": ["@ehendrix23", "@bramkragten", "@bdraco"],
|
"codeowners": ["@ehendrix23", "@bramkragten", "@bdraco"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Support for interfacing with the XBMC/Kodi JSON-RPC API."""
|
"""Support for interfacing with the XBMC/Kodi JSON-RPC API."""
|
||||||
|
import asyncio
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from datetime import timedelta
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
@ -53,6 +55,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import config_validation as cv, script
|
from homeassistant.helpers import config_validation as cv, script
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.util.yaml import dump
|
from homeassistant.util.yaml import dump
|
||||||
@ -82,6 +85,8 @@ DEPRECATED_TURN_OFF_ACTIONS = {
|
|||||||
"shutdown": "System.Shutdown",
|
"shutdown": "System.Shutdown",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WEBSOCKET_WATCHDOG_INTERVAL = timedelta(minutes=3)
|
||||||
|
|
||||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/media/MediaType.h
|
# https://github.com/xbmc/xbmc/blob/master/xbmc/media/MediaType.h
|
||||||
MEDIA_TYPES = {
|
MEDIA_TYPES = {
|
||||||
"music": MEDIA_TYPE_MUSIC,
|
"music": MEDIA_TYPE_MUSIC,
|
||||||
@ -435,6 +440,26 @@ class KodiDevice(MediaPlayerEntity):
|
|||||||
# run until the websocket connection is closed.
|
# run until the websocket connection is closed.
|
||||||
self.hass.loop.create_task(ws_loop_wrapper())
|
self.hass.loop.create_task(ws_loop_wrapper())
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""Connect the websocket if needed."""
|
||||||
|
if not self._enable_websocket:
|
||||||
|
return
|
||||||
|
|
||||||
|
asyncio.create_task(self.async_ws_connect())
|
||||||
|
|
||||||
|
self.async_on_remove(
|
||||||
|
async_track_time_interval(
|
||||||
|
self.hass,
|
||||||
|
self._async_connect_websocket_if_disconnected,
|
||||||
|
WEBSOCKET_WATCHDOG_INTERVAL,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_connect_websocket_if_disconnected(self, *_):
|
||||||
|
"""Reconnect the websocket if it fails."""
|
||||||
|
if not self._ws_server.connected:
|
||||||
|
await self.async_ws_connect()
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Retrieve latest state."""
|
"""Retrieve latest state."""
|
||||||
self._players = await self._get_players()
|
self._players = await self._get_players()
|
||||||
@ -445,9 +470,6 @@ class KodiDevice(MediaPlayerEntity):
|
|||||||
self._app_properties = {}
|
self._app_properties = {}
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._enable_websocket and not self._ws_server.connected:
|
|
||||||
self.hass.async_create_task(self.async_ws_connect())
|
|
||||||
|
|
||||||
self._app_properties = await self.server.Application.GetProperties(
|
self._app_properties = await self.server.Application.GetProperties(
|
||||||
["volume", "muted"]
|
["volume", "muted"]
|
||||||
)
|
)
|
||||||
|
@ -216,6 +216,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
existing_host_port_aliases = {
|
existing_host_port_aliases = {
|
||||||
_format_host_port_alias(entry.data)
|
_format_host_port_alias(entry.data)
|
||||||
for entry in self._async_current_entries()
|
for entry in self._async_current_entries()
|
||||||
|
if CONF_HOST in entry.data
|
||||||
}
|
}
|
||||||
return _format_host_port_alias(user_input) in existing_host_port_aliases
|
return _format_host_port_alias(user_input) in existing_host_port_aliases
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ class PlexServer:
|
|||||||
self._plextv_clients = [
|
self._plextv_clients = [
|
||||||
x
|
x
|
||||||
for x in self.account.resources()
|
for x in self.account.resources()
|
||||||
if "player" in x.provides and x.presence
|
if "player" in x.provides and x.presence and x.publicAddressMatches
|
||||||
]
|
]
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Current available clients from plex.tv: %s", self._plextv_clients
|
"Current available clients from plex.tv: %s", self._plextv_clients
|
||||||
|
@ -100,7 +100,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
tasks = [sensor.async_update() for sensor in sensors]
|
tasks = [sensor.async_update() for sensor in sensors]
|
||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks)
|
await asyncio.wait(tasks)
|
||||||
if not all(sensor.data.departures for sensor in sensors):
|
|
||||||
|
if not any(sensor.data for sensor in sensors):
|
||||||
raise PlatformNotReady
|
raise PlatformNotReady
|
||||||
|
|
||||||
async_add_entities(sensors)
|
async_add_entities(sensors)
|
||||||
@ -165,6 +166,7 @@ class RMVDepartureSensor(Entity):
|
|||||||
"minutes": self.data.departures[0].get("minutes"),
|
"minutes": self.data.departures[0].get("minutes"),
|
||||||
"departure_time": self.data.departures[0].get("departure_time"),
|
"departure_time": self.data.departures[0].get("departure_time"),
|
||||||
"product": self.data.departures[0].get("product"),
|
"product": self.data.departures[0].get("product"),
|
||||||
|
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||||
}
|
}
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return {}
|
return {}
|
||||||
@ -183,13 +185,16 @@ class RMVDepartureSensor(Entity):
|
|||||||
"""Get the latest data and update the state."""
|
"""Get the latest data and update the state."""
|
||||||
await self.data.async_update()
|
await self.data.async_update()
|
||||||
|
|
||||||
|
if self._name == DEFAULT_NAME:
|
||||||
|
self._name = self.data.station
|
||||||
|
|
||||||
|
self._station = self.data.station
|
||||||
|
|
||||||
if not self.data.departures:
|
if not self.data.departures:
|
||||||
self._state = None
|
self._state = None
|
||||||
self._icon = ICONS[None]
|
self._icon = ICONS[None]
|
||||||
return
|
return
|
||||||
if self._name == DEFAULT_NAME:
|
|
||||||
self._name = self.data.station
|
|
||||||
self._station = self.data.station
|
|
||||||
self._state = self.data.departures[0].get("minutes")
|
self._state = self.data.departures[0].get("minutes")
|
||||||
self._icon = ICONS[self.data.departures[0].get("product")]
|
self._icon = ICONS[self.data.departures[0].get("product")]
|
||||||
|
|
||||||
@ -220,6 +225,7 @@ class RMVDepartureData:
|
|||||||
self._max_journeys = max_journeys
|
self._max_journeys = max_journeys
|
||||||
self.rmv = RMVtransport(session, timeout)
|
self.rmv = RMVtransport(session, timeout)
|
||||||
self.departures = []
|
self.departures = []
|
||||||
|
self._error_notification = False
|
||||||
|
|
||||||
@Throttle(SCAN_INTERVAL)
|
@Throttle(SCAN_INTERVAL)
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
@ -231,31 +237,49 @@ class RMVDepartureData:
|
|||||||
direction_id=self._direction,
|
direction_id=self._direction,
|
||||||
max_journeys=50,
|
max_journeys=50,
|
||||||
)
|
)
|
||||||
|
|
||||||
except RMVtransportApiConnectionError:
|
except RMVtransportApiConnectionError:
|
||||||
self.departures = []
|
self.departures = []
|
||||||
_LOGGER.warning("Could not retrieve data from rmv.de")
|
_LOGGER.warning("Could not retrieve data from rmv.de")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.station = _data.get("station")
|
self.station = _data.get("station")
|
||||||
|
|
||||||
_deps = []
|
_deps = []
|
||||||
|
_deps_not_found = set(self._destinations)
|
||||||
|
|
||||||
for journey in _data["journeys"]:
|
for journey in _data["journeys"]:
|
||||||
# find the first departure meeting the criteria
|
# find the first departure meeting the criteria
|
||||||
_nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION}
|
_nextdep = {}
|
||||||
if self._destinations:
|
if self._destinations:
|
||||||
dest_found = False
|
dest_found = False
|
||||||
for dest in self._destinations:
|
for dest in self._destinations:
|
||||||
if dest in journey["stops"]:
|
if dest in journey["stops"]:
|
||||||
dest_found = True
|
dest_found = True
|
||||||
|
if dest in _deps_not_found:
|
||||||
|
_deps_not_found.remove(dest)
|
||||||
_nextdep["destination"] = dest
|
_nextdep["destination"] = dest
|
||||||
|
|
||||||
if not dest_found:
|
if not dest_found:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif self._lines and journey["number"] not in self._lines:
|
elif self._lines and journey["number"] not in self._lines:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif journey["minutes"] < self._time_offset:
|
elif journey["minutes"] < self._time_offset:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for attr in ["direction", "departure_time", "product", "minutes"]:
|
for attr in ["direction", "departure_time", "product", "minutes"]:
|
||||||
_nextdep[attr] = journey.get(attr, "")
|
_nextdep[attr] = journey.get(attr, "")
|
||||||
|
|
||||||
_nextdep["line"] = journey.get("number", "")
|
_nextdep["line"] = journey.get("number", "")
|
||||||
_deps.append(_nextdep)
|
_deps.append(_nextdep)
|
||||||
|
|
||||||
if len(_deps) > self._max_journeys:
|
if len(_deps) > self._max_journeys:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if not self._error_notification and _deps_not_found:
|
||||||
|
self._error_notification = True
|
||||||
|
_LOGGER.info("Destination(s) %s not found", ", ".join(_deps_not_found))
|
||||||
|
|
||||||
self.departures = _deps
|
self.departures = _deps
|
||||||
|
@ -9,6 +9,7 @@ import voluptuous as vol
|
|||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import ssdp
|
from homeassistant.components import ssdp
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
from .const import CONF_ENDPOINT, DOMAIN # pylint: disable=unused-import
|
from .const import CONF_ENDPOINT, DOMAIN # pylint: disable=unused-import
|
||||||
|
|
||||||
@ -74,7 +75,7 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
async def async_step_init(self, user_input=None):
|
async def async_step_init(self, user_input=None):
|
||||||
"""Handle a flow start."""
|
"""Handle a flow start."""
|
||||||
# Check if already configured
|
# Check if already configured
|
||||||
if self._endpoint_already_configured():
|
if self._async_endpoint_already_configured():
|
||||||
return self.async_abort(reason="already_configured")
|
return self.async_abort(reason="already_configured")
|
||||||
|
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
@ -145,9 +146,10 @@ class SongpalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
return await self.async_step_init(user_input)
|
return await self.async_step_init(user_input)
|
||||||
|
|
||||||
def _endpoint_already_configured(self):
|
@callback
|
||||||
|
def _async_endpoint_already_configured(self):
|
||||||
"""See if we already have an endpoint matching user input configured."""
|
"""See if we already have an endpoint matching user input configured."""
|
||||||
existing_endpoints = [
|
for entry in self._async_current_entries():
|
||||||
entry.data[CONF_ENDPOINT] for entry in self._async_current_entries()
|
if entry.data.get(CONF_ENDPOINT) == self.conf.endpoint:
|
||||||
]
|
return True
|
||||||
return self.conf.endpoint in existing_endpoints
|
return False
|
||||||
|
@ -71,6 +71,9 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
self.entry.data[CONF_WEBHOOK_ID]
|
self.entry.data[CONF_WEBHOOK_ID]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Ensure the webhook is not registered already
|
||||||
|
webhook_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID])
|
||||||
|
|
||||||
webhook_register(
|
webhook_register(
|
||||||
self.hass,
|
self.hass,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Constants used by Home Assistant components."""
|
"""Constants used by Home Assistant components."""
|
||||||
MAJOR_VERSION = 0
|
MAJOR_VERSION = 0
|
||||||
MINOR_VERSION = 113
|
MINOR_VERSION = 113
|
||||||
PATCH_VERSION = "2"
|
PATCH_VERSION = "3"
|
||||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER = (3, 7, 1)
|
REQUIRED_PYTHON_VER = (3, 7, 1)
|
||||||
|
@ -27,6 +27,7 @@ ruamel.yaml==0.15.100
|
|||||||
sqlalchemy==1.3.18
|
sqlalchemy==1.3.18
|
||||||
voluptuous-serialize==2.4.0
|
voluptuous-serialize==2.4.0
|
||||||
voluptuous==0.11.7
|
voluptuous==0.11.7
|
||||||
|
yarl==1.4.2
|
||||||
zeroconf==0.27.1
|
zeroconf==0.27.1
|
||||||
|
|
||||||
pycryptodome>=3.6.6
|
pycryptodome>=3.6.6
|
||||||
|
@ -20,3 +20,4 @@ requests==2.24.0
|
|||||||
ruamel.yaml==0.15.100
|
ruamel.yaml==0.15.100
|
||||||
voluptuous==0.11.7
|
voluptuous==0.11.7
|
||||||
voluptuous-serialize==2.4.0
|
voluptuous-serialize==2.4.0
|
||||||
|
yarl==1.4.2
|
||||||
|
@ -100,7 +100,7 @@ WazeRouteCalculator==0.12
|
|||||||
YesssSMS==0.4.1
|
YesssSMS==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.abode
|
# homeassistant.components.abode
|
||||||
abodepy==0.19.0
|
abodepy==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.mcp23017
|
# homeassistant.components.mcp23017
|
||||||
adafruit-blinka==3.9.0
|
adafruit-blinka==3.9.0
|
||||||
@ -112,7 +112,7 @@ adafruit-circuitpython-bmp280==3.1.1
|
|||||||
adafruit-circuitpython-mcp230xx==2.2.2
|
adafruit-circuitpython-mcp230xx==2.2.2
|
||||||
|
|
||||||
# homeassistant.components.androidtv
|
# homeassistant.components.androidtv
|
||||||
adb-shell[async]==0.2.0
|
adb-shell[async]==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.alarmdecoder
|
# homeassistant.components.alarmdecoder
|
||||||
adext==0.3
|
adext==0.3
|
||||||
@ -164,7 +164,7 @@ aioftp==0.12.0
|
|||||||
aioguardian==1.0.1
|
aioguardian==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.harmony
|
# homeassistant.components.harmony
|
||||||
aioharmony==0.2.5
|
aioharmony==0.2.6
|
||||||
|
|
||||||
# homeassistant.components.homekit_controller
|
# homeassistant.components.homekit_controller
|
||||||
aiohomekit[IP]==0.2.45
|
aiohomekit[IP]==0.2.45
|
||||||
@ -231,7 +231,7 @@ ambiclimate==0.2.1
|
|||||||
amcrest==1.7.0
|
amcrest==1.7.0
|
||||||
|
|
||||||
# homeassistant.components.androidtv
|
# homeassistant.components.androidtv
|
||||||
androidtv[async]==0.0.46
|
androidtv[async]==0.0.47
|
||||||
|
|
||||||
# homeassistant.components.anel_pwrctrl
|
# homeassistant.components.anel_pwrctrl
|
||||||
anel_pwrctrl-homeassistant==0.0.1.dev2
|
anel_pwrctrl-homeassistant==0.0.1.dev2
|
||||||
@ -1190,7 +1190,7 @@ py_nextbusnext==0.1.4
|
|||||||
# py_noaa==0.3.0
|
# py_noaa==0.3.0
|
||||||
|
|
||||||
# homeassistant.components.ads
|
# homeassistant.components.ads
|
||||||
pyads==3.1.3
|
pyads==3.2.1
|
||||||
|
|
||||||
# homeassistant.components.hisense_aehw4a1
|
# homeassistant.components.hisense_aehw4a1
|
||||||
pyaehw4a1==0.3.5
|
pyaehw4a1==0.3.5
|
||||||
@ -1241,7 +1241,7 @@ pycfdns==0.0.1
|
|||||||
pychannels==1.0.0
|
pychannels==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.cast
|
# homeassistant.components.cast
|
||||||
pychromecast==7.1.2
|
pychromecast==7.2.0
|
||||||
|
|
||||||
# homeassistant.components.cmus
|
# homeassistant.components.cmus
|
||||||
pycmus==0.1.1
|
pycmus==0.1.1
|
||||||
|
@ -43,10 +43,10 @@ WSDiscovery==2.0.0
|
|||||||
YesssSMS==0.4.1
|
YesssSMS==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.abode
|
# homeassistant.components.abode
|
||||||
abodepy==0.19.0
|
abodepy==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.androidtv
|
# homeassistant.components.androidtv
|
||||||
adb-shell[async]==0.2.0
|
adb-shell[async]==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.adguard
|
# homeassistant.components.adguard
|
||||||
adguardhome==0.4.2
|
adguardhome==0.4.2
|
||||||
@ -89,7 +89,7 @@ aiofreepybox==0.0.8
|
|||||||
aioguardian==1.0.1
|
aioguardian==1.0.1
|
||||||
|
|
||||||
# homeassistant.components.harmony
|
# homeassistant.components.harmony
|
||||||
aioharmony==0.2.5
|
aioharmony==0.2.6
|
||||||
|
|
||||||
# homeassistant.components.homekit_controller
|
# homeassistant.components.homekit_controller
|
||||||
aiohomekit[IP]==0.2.45
|
aiohomekit[IP]==0.2.45
|
||||||
@ -132,7 +132,7 @@ airly==0.0.2
|
|||||||
ambiclimate==0.2.1
|
ambiclimate==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.androidtv
|
# homeassistant.components.androidtv
|
||||||
androidtv[async]==0.0.46
|
androidtv[async]==0.0.47
|
||||||
|
|
||||||
# homeassistant.components.apns
|
# homeassistant.components.apns
|
||||||
apns2==0.3.0
|
apns2==0.3.0
|
||||||
@ -577,7 +577,7 @@ pyblackbird==0.5
|
|||||||
pybotvac==0.0.17
|
pybotvac==0.0.17
|
||||||
|
|
||||||
# homeassistant.components.cast
|
# homeassistant.components.cast
|
||||||
pychromecast==7.1.2
|
pychromecast==7.2.0
|
||||||
|
|
||||||
# homeassistant.components.coolmaster
|
# homeassistant.components.coolmaster
|
||||||
pycoolmasternet==0.0.4
|
pycoolmasternet==0.0.4
|
||||||
|
1
setup.py
1
setup.py
@ -52,6 +52,7 @@ REQUIRES = [
|
|||||||
"ruamel.yaml==0.15.100",
|
"ruamel.yaml==0.15.100",
|
||||||
"voluptuous==0.11.7",
|
"voluptuous==0.11.7",
|
||||||
"voluptuous-serialize==2.4.0",
|
"voluptuous-serialize==2.4.0",
|
||||||
|
"yarl==1.4.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER))
|
MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER))
|
||||||
|
@ -38,3 +38,33 @@ async def test_capture_image(hass):
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
mock_capture.assert_called_once()
|
mock_capture.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_camera_on(hass):
|
||||||
|
"""Test the camera turn on service."""
|
||||||
|
await setup_platform(hass, CAMERA_DOMAIN)
|
||||||
|
|
||||||
|
with patch("abodepy.AbodeCamera.privacy_mode") as mock_capture:
|
||||||
|
await hass.services.async_call(
|
||||||
|
CAMERA_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{ATTR_ENTITY_ID: "camera.test_cam"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
mock_capture.assert_called_once_with(False)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_camera_off(hass):
|
||||||
|
"""Test the camera turn off service."""
|
||||||
|
await setup_platform(hass, CAMERA_DOMAIN)
|
||||||
|
|
||||||
|
with patch("abodepy.AbodeCamera.privacy_mode") as mock_capture:
|
||||||
|
await hass.services.async_call(
|
||||||
|
CAMERA_DOMAIN,
|
||||||
|
"turn_off",
|
||||||
|
{ATTR_ENTITY_ID: "camera.test_cam"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
mock_capture.assert_called_once_with(True)
|
||||||
|
@ -13,7 +13,9 @@ async def test_creating_entry_sets_up_media_player(hass):
|
|||||||
"homeassistant.components.cast.media_player.async_setup_entry",
|
"homeassistant.components.cast.media_player.async_setup_entry",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_setup, patch(
|
) as mock_setup, patch(
|
||||||
"pychromecast.discovery.discover_chromecasts", return_value=True
|
"pychromecast.discovery.discover_chromecasts", return_value=(True, None)
|
||||||
|
), patch(
|
||||||
|
"pychromecast.discovery.stop_discovery"
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
cast.DOMAIN, context={"source": config_entries.SOURCE_USER}
|
cast.DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
@ -34,9 +36,7 @@ async def test_configuring_cast_creates_entry(hass):
|
|||||||
"""Test that specifying config will create an entry."""
|
"""Test that specifying config will create an entry."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.cast.async_setup_entry", return_value=True
|
"homeassistant.components.cast.async_setup_entry", return_value=True
|
||||||
) as mock_setup, patch(
|
) as mock_setup:
|
||||||
"pychromecast.discovery.discover_chromecasts", return_value=True
|
|
||||||
):
|
|
||||||
await async_setup_component(
|
await async_setup_component(
|
||||||
hass, cast.DOMAIN, {"cast": {"some_config": "to_trigger_import"}}
|
hass, cast.DOMAIN, {"cast": {"some_config": "to_trigger_import"}}
|
||||||
)
|
)
|
||||||
@ -49,9 +49,7 @@ async def test_not_configuring_cast_not_creates_entry(hass):
|
|||||||
"""Test that no config will not create an entry."""
|
"""Test that no config will not create an entry."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.cast.async_setup_entry", return_value=True
|
"homeassistant.components.cast.async_setup_entry", return_value=True
|
||||||
) as mock_setup, patch(
|
) as mock_setup:
|
||||||
"pychromecast.discovery.discover_chromecasts", return_value=True
|
|
||||||
):
|
|
||||||
await async_setup_component(hass, cast.DOMAIN, {})
|
await async_setup_component(hass, cast.DOMAIN, {})
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -153,6 +153,9 @@ async def test_form_ssdp_aborts_before_checking_remoteid_if_host_known(hass):
|
|||||||
)
|
)
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
config_entry_without_host = MockConfigEntry(domain=DOMAIN, data={"name": "other"},)
|
||||||
|
config_entry_without_host.add_to_hass(hass)
|
||||||
|
|
||||||
harmonyapi = _get_mock_harmonyapi(connect=True)
|
harmonyapi = _get_mock_harmonyapi(connect=True)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
|
@ -247,7 +247,25 @@ async def test_form_import_dupe(hass):
|
|||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": "import"}, data=VALID_CONFIG
|
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=VALID_CONFIG
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_import_with_ignored_entry(hass):
|
||||||
|
"""Test we get abort on duplicate import when there is an ignored one."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
ignored_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE
|
||||||
|
)
|
||||||
|
ignored_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=VALID_CONFIG
|
||||||
)
|
)
|
||||||
assert result["type"] == "abort"
|
assert result["type"] == "abort"
|
||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
@ -53,6 +53,7 @@ class MockResource:
|
|||||||
self.provides = ["player"]
|
self.provides = ["player"]
|
||||||
self.device = MockPlexClient(f"http://192.168.0.1{index}:32500", index + 10)
|
self.device = MockPlexClient(f"http://192.168.0.1{index}:32500", index + 10)
|
||||||
self.presence = index == 0
|
self.presence = index == 0
|
||||||
|
self.publicAddressMatches = True
|
||||||
|
|
||||||
def connect(self, timeout):
|
def connect(self, timeout):
|
||||||
"""Mock the resource connect method."""
|
"""Mock the resource connect method."""
|
||||||
|
@ -48,7 +48,7 @@ VALID_CONFIG_DEST = {
|
|||||||
|
|
||||||
def get_departures_mock():
|
def get_departures_mock():
|
||||||
"""Mock rmvtransport departures loading."""
|
"""Mock rmvtransport departures loading."""
|
||||||
data = {
|
return {
|
||||||
"station": "Frankfurt (Main) Hauptbahnhof",
|
"station": "Frankfurt (Main) Hauptbahnhof",
|
||||||
"stationId": "3000010",
|
"stationId": "3000010",
|
||||||
"filter": "11111111111",
|
"filter": "11111111111",
|
||||||
@ -145,18 +145,16 @@ def get_departures_mock():
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def get_no_departures_mock():
|
def get_no_departures_mock():
|
||||||
"""Mock no departures in results."""
|
"""Mock no departures in results."""
|
||||||
data = {
|
return {
|
||||||
"station": "Frankfurt (Main) Hauptbahnhof",
|
"station": "Frankfurt (Main) Hauptbahnhof",
|
||||||
"stationId": "3000010",
|
"stationId": "3000010",
|
||||||
"filter": "11111111111",
|
"filter": "11111111111",
|
||||||
"journeys": [],
|
"journeys": [],
|
||||||
}
|
}
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def test_rmvtransport_min_config(hass):
|
async def test_rmvtransport_min_config(hass):
|
||||||
@ -232,4 +230,4 @@ async def test_rmvtransport_no_departures(hass):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
state = hass.states.get("sensor.frankfurt_main_hauptbahnhof")
|
state = hass.states.get("sensor.frankfurt_main_hauptbahnhof")
|
||||||
assert not state
|
assert state.state == "unavailable"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user