Compare commits

..

24 Commits

Author SHA1 Message Date
Franck Nijhof
d520a02b8c Merge pull request #32932 from home-assistant/rc
0.107.0
2020-03-18 15:35:44 +01:00
SukramJ
1e469b39ad Fix flaky tests for HMIPC (#32806) 2020-03-18 14:44:29 +01:00
Franck Nijhof
c2f615839d Bumped version to 0.107.0 2020-03-18 13:31:02 +01:00
Bram Kragten
657bf33e32 Updated frontend to 20200318.0 (#32931) 2020-03-18 13:27:21 +01:00
Paulus Schoutsen
0ca87007fd Bumped version to 0.107.0b8 2020-03-17 17:56:18 -07:00
Paulus Schoutsen
d0d9d853f2 Fix hassio panel load (#32922)
* Fix loading hassio panel

* Remove blacklist
2020-03-17 17:55:57 -07:00
Hans Oischinger
8348878e7e Introduce safe scan_interval for vicare (#32915) 2020-03-17 17:55:56 -07:00
Paulus Schoutsen
b70be5f2f2 Blacklist auto_backup (#32912)
* Blacklist auto_backup

* Mock with a set
2020-03-17 17:55:55 -07:00
Bram Kragten
fddb565e4c Fix input text reload (#32911)
* Fix input text reload

* FIx schema instead
2020-03-17 17:55:54 -07:00
Rami Mosleh
f3e6820042 Fix setting up options due to config data freeze (#32872) 2020-03-17 17:55:54 -07:00
Paulus Schoutsen
ae98f13181 Bumped version to 0.107.0b7 2020-03-17 10:36:59 -07:00
Paulus Schoutsen
ab38e7d98a Bump cast to 4.2.0 (#32906) 2020-03-17 10:35:39 -07:00
brubaked
9797b09d44 Changed Sensor icons to be more emotionally sensitive (#32904)
The existing sensor icons, while descriptive - dead = dead - are perhaps too matter of fact and don't accurately convey the tragedy. I changed emoticon-dead-outline to emoticon-cry-outline, as I think it better conveys the reality of the situation along with the emotions tied to the statistic.
2020-03-17 10:35:39 -07:00
Quentame
4908d4358c Bump iCloud to 0.9.5 (#32901) 2020-03-17 10:35:38 -07:00
Paulus Schoutsen
67d728fc50 Make zone dependency of device tracker an after dep (#32880)
* Make zone dependency of device tracker an after dep

* Fix test
2020-03-17 10:34:34 -07:00
Jason Lachowsky
912409ed0c Corrected minor misspellings (#32857) 2020-03-17 10:32:01 -07:00
Paolo Tuninetto
ac8c889b0f Add default port to samsung tv (#32820)
* Default port for websocket tv

* Update config entry

* move bridge creation

* fix indent

* remove loop
2020-03-17 10:32:00 -07:00
Quentame
67a721d39b Fix iCloud init while pending (#32750)
* Fix iCloud init while pending

Continue if device is pending while setup
Create devices and fetch 15s if pending, otherwise determine interval to fetch.

* Add retried_fetch guard
2020-03-17 10:32:00 -07:00
Paulus Schoutsen
d196fd136d Bumped version to 0.107.0b6 2020-03-16 14:53:13 -07:00
Bram Kragten
4f78674a4c Updated frontend to 20200316.1 (#32878) 2020-03-16 14:53:05 -07:00
Bram Kragten
a7aca10668 Lovelace: storage key based on id instead of url_path (#32873)
* Fix storage key based on url_path

* Fix test
2020-03-16 14:53:04 -07:00
Paulus Schoutsen
03b1c6ddee Remove group as a dependency from entity integrations (#32870)
* remove group dependency

* Update device sun light trigger

* Add zone dep back to device tracker
2020-03-16 14:53:03 -07:00
David F. Mulcahey
661f1b69f2 Bump ZHA quirks to 0.0.37 (#32867) 2020-03-16 14:53:03 -07:00
Bram Kragten
ccb34083fe Add lovelace reload service for yaml resources (#32865)
* Lovelace add reload service for yaml resources

* Clean up imports

* Comments
2020-03-16 14:53:02 -07:00
50 changed files with 250 additions and 171 deletions

View File

@@ -3,7 +3,8 @@
"name": "Automation",
"documentation": "https://www.home-assistant.io/integrations/automation",
"requirements": [],
"dependencies": ["device_automation", "group", "webhook"],
"dependencies": [],
"after_dependencies": ["device_automation", "webhook"],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal"
}

View File

@@ -3,7 +3,7 @@
"name": "Google Cast",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"requirements": ["pychromecast==4.1.1"],
"requirements": ["pychromecast==4.2.0"],
"dependencies": [],
"after_dependencies": ["cloud"],
"zeroconf": ["_googlecast._tcp.local."],

View File

@@ -7,9 +7,9 @@ from .const import ATTRIBUTION, OPTION_WORLDWIDE
SENSORS = {
"confirmed": "mdi:emoticon-neutral-outline",
"current": "mdi:emoticon-frown-outline",
"current": "mdi:emoticon-sad-outline",
"recovered": "mdi:emoticon-happy-outline",
"deaths": "mdi:emoticon-dead-outline",
"deaths": "mdi:emoticon-cry-outline",
}

View File

@@ -3,7 +3,7 @@
"name": "Cover",
"documentation": "https://www.home-assistant.io/integrations/cover",
"requirements": [],
"dependencies": ["group"],
"dependencies": [],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal"
}

View File

@@ -3,7 +3,8 @@
"name": "Presence-based Lights",
"documentation": "https://www.home-assistant.io/integrations/device_sun_light_trigger",
"requirements": [],
"dependencies": ["device_tracker", "group", "light", "person"],
"dependencies": [],
"after_dependencies": ["device_tracker", "group", "light", "person"],
"codeowners": [],
"quality_scale": "internal"
}

View File

@@ -3,7 +3,8 @@
"name": "Device Tracker",
"documentation": "https://www.home-assistant.io/integrations/device_tracker",
"requirements": [],
"dependencies": ["group", "zone"],
"dependencies": [],
"after_dependencies": ["zone"],
"codeowners": [],
"quality_scale": "internal"
}

View File

@@ -3,7 +3,7 @@
"name": "Fan",
"documentation": "https://www.home-assistant.io/integrations/fan",
"requirements": [],
"dependencies": ["group"],
"dependencies": [],
"codeowners": [],
"quality_scale": "internal"
}

View File

@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20200316.0"
"home-assistant-frontend==20200318.0"
],
"dependencies": [
"api",

View File

@@ -190,16 +190,15 @@ async def async_setup(hass, config):
hass.http.register_view(HassIOView(host, websession))
if "frontend" in hass.config.components:
await hass.components.panel_custom.async_register_panel(
frontend_url_path="hassio",
webcomponent_name="hassio-main",
sidebar_title="Supervisor",
sidebar_icon="hass:home-assistant",
js_url="/api/hassio/app/entrypoint.js",
embed_iframe=True,
require_admin=True,
)
await hass.components.panel_custom.async_register_panel(
frontend_url_path="hassio",
webcomponent_name="hassio-main",
sidebar_title="Supervisor",
sidebar_icon="hass:home-assistant",
js_url="/api/hassio/app/entrypoint.js",
embed_iframe=True,
require_admin=True,
)
await hassio.update_hass_api(config.get("http", {}), refresh_token)

View File

@@ -14,7 +14,7 @@
"busy_error": "Device refused to add pairing as it is already pairing with another controller.",
"max_peers_error": "Device refused to add pairing as it has no free pairing storage.",
"max_tries_error": "Device refused to add pairing as it has received more than 100 unsuccessful authentication attempts.",
"pairing_failed": "An unhandled error occured while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently.",
"pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently.",
"unable_to_pair": "Unable to pair, please try again.",
"unknown_error": "Device reported an unknown error. Pairing failed."
},

View File

@@ -25,7 +25,7 @@
"max_peers_error": "Device refused to add pairing as it has no free pairing storage.",
"busy_error": "Device refused to add pairing as it is already pairing with another controller.",
"max_tries_error": "Device refused to add pairing as it has received more than 100 unsuccessful authentication attempts.",
"pairing_failed": "An unhandled error occured while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently."
"pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently."
},
"abort": {
"no_devices": "No unpaired devices could be found",

View File

@@ -97,6 +97,7 @@ class IcloudAccount:
self._owner_fullname = None
self._family_members_fullname = {}
self._devices = {}
self._retried_fetch = False
self.listeners = []
@@ -122,10 +123,6 @@ class IcloudAccount:
_LOGGER.error("No iCloud device found")
raise ConfigEntryNotReady
if DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending":
_LOGGER.warning("Pending devices, trying again ...")
raise ConfigEntryNotReady
self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}"
self._family_members_fullname = {}
@@ -157,28 +154,15 @@ class IcloudAccount:
)
return
if DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending":
_LOGGER.warning("Pending devices, trying again in 15s")
self._fetch_interval = 0.25
dispatcher_send(self.hass, self.signal_device_update)
track_point_in_utc_time(
self.hass,
self.keep_alive,
utcnow() + timedelta(minutes=self._fetch_interval),
)
return
# Gets devices infos
new_device = False
for device in api_devices:
status = device.status(DEVICE_STATUS_SET)
device_id = status[DEVICE_ID]
device_name = status[DEVICE_NAME]
device_status = DEVICE_STATUS_CODES.get(status[DEVICE_STATUS], "error")
if (
device_status == "pending"
or status[DEVICE_BATTERY_STATUS] == "Unknown"
status[DEVICE_BATTERY_STATUS] == "Unknown"
or status.get(DEVICE_BATTERY_LEVEL) is None
):
continue
@@ -198,7 +182,16 @@ class IcloudAccount:
self._devices[device_id].update(status)
new_device = True
self._fetch_interval = self._determine_interval()
if (
DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending"
and not self._retried_fetch
):
_LOGGER.warning("Pending devices, trying again in 15s")
self._fetch_interval = 0.25
self._retried_fetch = True
else:
self._fetch_interval = self._determine_interval()
self._retried_fetch = False
dispatcher_send(self.hass, self.signal_device_update)
if new_device:

View File

@@ -3,7 +3,7 @@
"name": "Apple iCloud",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/icloud",
"requirements": ["pyicloud==0.9.4"],
"requirements": ["pyicloud==0.9.5"],
"dependencies": [],
"codeowners": ["@Quentame"]
}

View File

@@ -88,24 +88,22 @@ def _cv_input_text(cfg):
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: cv.schema_with_slug_keys(
vol.Any(
vol.All(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int),
vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int),
vol.Optional(CONF_INITIAL, ""): cv.string,
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_PATTERN): cv.string,
vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In(
[MODE_TEXT, MODE_PASSWORD]
),
},
_cv_input_text,
),
None,
)
vol.All(
lambda value: value or {},
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int),
vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int),
vol.Optional(CONF_INITIAL, ""): cv.string,
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_PATTERN): cv.string,
vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In(
[MODE_TEXT, MODE_PASSWORD]
),
},
_cv_input_text,
),
)
},
extra=vol.ALLOW_EXTRA,
@@ -203,13 +201,6 @@ class InputText(RestoreEntity):
@classmethod
def from_yaml(cls, config: typing.Dict) -> "InputText":
"""Return entity instance initialized from yaml storage."""
# set defaults for empty config
config = {
CONF_MAX: CONF_MAX_VALUE,
CONF_MIN: CONF_MIN_VALUE,
CONF_MODE: MODE_TEXT,
**config,
}
input_text = cls(config)
input_text.entity_id = f"{DOMAIN}.{config[CONF_ID]}"
input_text.editable = False

View File

@@ -3,7 +3,7 @@
"name": "Light",
"documentation": "https://www.home-assistant.io/integrations/light",
"requirements": [],
"dependencies": ["group"],
"dependencies": [],
"codeowners": [],
"quality_scale": "internal"
}

View File

@@ -3,7 +3,7 @@
"name": "Lock",
"documentation": "https://www.home-assistant.io/integrations/lock",
"requirements": [],
"dependencies": ["group"],
"dependencies": [],
"codeowners": [],
"quality_scale": "internal"
}

View File

@@ -4,10 +4,14 @@ import logging
import voluptuous as vol
from homeassistant.components import frontend
from homeassistant.config import async_hass_config_yaml, async_process_component_config
from homeassistant.const import CONF_FILENAME
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import collection, config_validation as cv
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType
from homeassistant.loader import async_get_integration
from homeassistant.util import sanitize_filename
from . import dashboard, resources, websocket
@@ -25,8 +29,10 @@ from .const import (
MODE_STORAGE,
MODE_YAML,
RESOURCE_CREATE_FIELDS,
RESOURCE_RELOAD_SERVICE_SCHEMA,
RESOURCE_SCHEMA,
RESOURCE_UPDATE_FIELDS,
SERVICE_RELOAD_RESOURCES,
STORAGE_DASHBOARD_CREATE_FIELDS,
STORAGE_DASHBOARD_UPDATE_FIELDS,
url_slug,
@@ -62,29 +68,41 @@ CONFIG_SCHEMA = vol.Schema(
)
async def async_setup(hass, config):
async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up the Lovelace commands."""
mode = config[DOMAIN][CONF_MODE]
yaml_resources = config[DOMAIN].get(CONF_RESOURCES)
frontend.async_register_built_in_panel(hass, DOMAIN, config={"mode": mode})
async def reload_resources_service_handler(service_call: ServiceCallType) -> None:
"""Reload yaml resources."""
try:
conf = await async_hass_config_yaml(hass)
except HomeAssistantError as err:
_LOGGER.error(err)
return
integration = await async_get_integration(hass, DOMAIN)
config = await async_process_component_config(hass, conf, integration)
resource_collection = await create_yaml_resource_col(
hass, config[DOMAIN].get(CONF_RESOURCES)
)
hass.data[DOMAIN]["resources"] = resource_collection
if mode == MODE_YAML:
default_config = dashboard.LovelaceYAML(hass, None, None)
resource_collection = await create_yaml_resource_col(hass, yaml_resources)
if yaml_resources is None:
try:
ll_conf = await default_config.async_load(False)
except HomeAssistantError:
pass
else:
if CONF_RESOURCES in ll_conf:
_LOGGER.warning(
"Resources need to be specified in your configuration.yaml. Please see the docs."
)
yaml_resources = ll_conf[CONF_RESOURCES]
resource_collection = resources.ResourceYAMLCollection(yaml_resources or [])
async_register_admin_service(
hass,
DOMAIN,
SERVICE_RELOAD_RESOURCES,
reload_resources_service_handler,
schema=RESOURCE_RELOAD_SERVICE_SCHEMA,
)
else:
default_config = dashboard.LovelaceStorage(hass, None)
@@ -196,6 +214,24 @@ async def async_setup(hass, config):
return True
async def create_yaml_resource_col(hass, yaml_resources):
"""Create yaml resources collection."""
if yaml_resources is None:
default_config = dashboard.LovelaceYAML(hass, None, None)
try:
ll_conf = await default_config.async_load(False)
except HomeAssistantError:
pass
else:
if CONF_RESOURCES in ll_conf:
_LOGGER.warning(
"Resources need to be specified in your configuration.yaml. Please see the docs."
)
yaml_resources = ll_conf[CONF_RESOURCES]
return resources.ResourceYAMLCollection(yaml_resources or [])
async def system_health_info(hass):
"""Get info for the info page."""
return await hass.data[DOMAIN]["dashboards"][None].async_get_info()

View File

@@ -41,6 +41,9 @@ RESOURCE_UPDATE_FIELDS = {
vol.Optional(CONF_URL): cv.string,
}
SERVICE_RELOAD_RESOURCES = "reload_resources"
RESOURCE_RELOAD_SERVICE_SCHEMA = vol.Schema({})
CONF_TITLE = "title"
CONF_REQUIRE_ADMIN = "require_admin"
CONF_SHOW_IN_SIDEBAR = "show_in_sidebar"

View File

@@ -88,7 +88,7 @@ class LovelaceStorage(LovelaceConfig):
storage_key = CONFIG_STORAGE_KEY_DEFAULT
else:
url_path = config[CONF_URL_PATH]
storage_key = CONFIG_STORAGE_KEY.format(url_path)
storage_key = CONFIG_STORAGE_KEY.format(config["id"])
super().__init__(hass, url_path, config)

View File

@@ -0,0 +1,4 @@
# Describes the format for available lovelace services
reload_resources:
description: Reload Lovelace resources from yaml configuration.

View File

@@ -332,16 +332,17 @@ class MikrotikHub:
async def async_add_options(self):
"""Populate default options for Mikrotik."""
if not self.config_entry.options:
data = dict(self.config_entry.data)
options = {
CONF_ARP_PING: self.config_entry.data.pop(CONF_ARP_PING, False),
CONF_FORCE_DHCP: self.config_entry.data.pop(CONF_FORCE_DHCP, False),
CONF_DETECTION_TIME: self.config_entry.data.pop(
CONF_ARP_PING: data.pop(CONF_ARP_PING, False),
CONF_FORCE_DHCP: data.pop(CONF_FORCE_DHCP, False),
CONF_DETECTION_TIME: data.pop(
CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME
),
}
self.hass.config_entries.async_update_entry(
self.config_entry, options=options
self.config_entry, data=data, options=options
)
async def request_update(self):

View File

@@ -3,7 +3,7 @@
"name": "Plant Monitor",
"documentation": "https://www.home-assistant.io/integrations/plant",
"requirements": [],
"dependencies": ["group", "zone"],
"dependencies": [],
"after_dependencies": ["recorder"],
"codeowners": ["@ChristianKuehnel"],
"quality_scale": "internal"

View File

@@ -3,6 +3,6 @@
"name": "Remote",
"documentation": "https://www.home-assistant.io/integrations/remote",
"requirements": [],
"dependencies": ["group"],
"dependencies": [],
"codeowners": []
}

View File

@@ -46,6 +46,7 @@ class SamsungTVBridge(ABC):
self.method = method
self.host = host
self.token = None
self.default_port = None
self._remote = None
self._callback = None
@@ -191,6 +192,7 @@ class SamsungTVWSBridge(SamsungTVBridge):
"""Initialize Bridge."""
super().__init__(method, host, port)
self.token = token
self.default_port = 8001
def try_connect(self):
"""Try to connect to the Websocket TV."""

View File

@@ -71,13 +71,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
):
turn_on_action = hass.data[DOMAIN][ip_address][CONF_ON_ACTION]
on_script = Script(hass, turn_on_action)
async_add_entities([SamsungTVDevice(config_entry, on_script)])
# Initialize bridge
data = config_entry.data.copy()
bridge = SamsungTVBridge.get_bridge(
data[CONF_METHOD], data[CONF_HOST], data[CONF_PORT], data.get(CONF_TOKEN),
)
if bridge.port is None and bridge.default_port is not None:
# For backward compat, set default port for websocket tv
data[CONF_PORT] = bridge.default_port
hass.config_entries.async_update_entry(config_entry, data=data)
bridge = SamsungTVBridge.get_bridge(
data[CONF_METHOD], data[CONF_HOST], data[CONF_PORT], data.get(CONF_TOKEN),
)
async_add_entities([SamsungTVDevice(bridge, config_entry, on_script)])
class SamsungTVDevice(MediaPlayerDevice):
"""Representation of a Samsung TV."""
def __init__(self, config_entry, on_script):
def __init__(self, bridge, config_entry, on_script):
"""Initialize the Samsung device."""
self._config_entry = config_entry
self._manufacturer = config_entry.data.get(CONF_MANUFACTURER)
@@ -93,13 +107,7 @@ class SamsungTVDevice(MediaPlayerDevice):
# Mark the end of a shutdown command (need to wait 15 seconds before
# sending the next command to avoid turning the TV back ON).
self._end_of_power_off = None
# Initialize bridge
self._bridge = SamsungTVBridge.get_bridge(
config_entry.data[CONF_METHOD],
config_entry.data[CONF_HOST],
config_entry.data[CONF_PORT],
config_entry.data.get(CONF_TOKEN),
)
self._bridge = bridge
self._bridge.register_reauth_callback(self.access_denied)
def access_denied(self):

View File

@@ -3,7 +3,7 @@
"name": "Scripts",
"documentation": "https://www.home-assistant.io/integrations/script",
"requirements": [],
"dependencies": ["group"],
"dependencies": [],
"codeowners": ["@home-assistant/core"],
"quality_scale": "internal"
}

View File

@@ -3,7 +3,7 @@
"name": "Switch",
"documentation": "https://www.home-assistant.io/integrations/switch",
"requirements": [],
"dependencies": ["group"],
"dependencies": [],
"codeowners": [],
"quality_scale": "internal"
}

View File

@@ -91,7 +91,7 @@ class LogEntry:
def __init__(self, record, stack, source):
"""Initialize a log entry."""
self.first_occured = self.timestamp = record.created
self.first_occurred = self.timestamp = record.created
self.name = record.name
self.level = record.levelname
self.message = deque([record.getMessage()], maxlen=5)
@@ -117,7 +117,7 @@ class LogEntry:
"timestamp": self.timestamp,
"exception": self.exception,
"count": self.count,
"first_occured": self.first_occured,
"first_occurred": self.first_occurred,
}

View File

@@ -5,7 +5,7 @@
"client_secret": "The client secret from the configuration is invalid.",
"no_agreements": "This account has no Toon displays.",
"no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/).",
"unknown_auth_fail": "Unexpected error occured, while authenticating."
"unknown_auth_fail": "Unexpected error occurred, while authenticating."
},
"error": {
"credentials": "The provided credentials are invalid.",

View File

@@ -26,7 +26,7 @@
"abort": {
"client_id": "The client ID from the configuration is invalid.",
"client_secret": "The client secret from the configuration is invalid.",
"unknown_auth_fail": "Unexpected error occured, while authenticating.",
"unknown_auth_fail": "Unexpected error occurred, while authenticating.",
"no_agreements": "This account has no Toon displays.",
"no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/)."
}

View File

@@ -3,6 +3,6 @@
"name": "Vacuum",
"documentation": "https://www.home-assistant.io/integrations/vacuum",
"requirements": [],
"dependencies": ["group"],
"dependencies": [],
"codeowners": []
}

View File

@@ -1,4 +1,5 @@
"""Viessmann ViCare climate device."""
from datetime import timedelta
import logging
import requests
@@ -79,6 +80,9 @@ HA_TO_VICARE_PRESET_HEATING = {
PYVICARE_ERROR = "error"
# Scan interval of 15 minutes seems to be safe to not hit the ViCare server rate limit
SCAN_INTERVAL = timedelta(seconds=900)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create the ViCare climate devices."""

View File

@@ -1,4 +1,5 @@
"""Viessmann ViCare water_heater device."""
from datetime import timedelta
import logging
import requests
@@ -42,6 +43,9 @@ HA_TO_VICARE_HVAC_DHW = {
PYVICARE_ERROR = "error"
# Scan interval of 15 minutes seems to be safe to not hit the ViCare server rate limit
SCAN_INTERVAL = timedelta(seconds=900)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create the ViCare water_heater devices."""

View File

@@ -5,7 +5,7 @@
"documentation": "https://www.home-assistant.io/integrations/zha",
"requirements": [
"bellows-homeassistant==0.14.0",
"zha-quirks==0.0.36",
"zha-quirks==0.0.37",
"zigpy-cc==0.1.0",
"zigpy-deconz==0.7.0",
"zigpy-homeassistant==0.16.0",

View File

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

View File

@@ -12,7 +12,7 @@ cryptography==2.8
defusedxml==0.6.0
distro==1.4.0
hass-nabucasa==0.32.2
home-assistant-frontend==20200316.0
home-assistant-frontend==20200318.0
importlib-metadata==1.5.0
jinja2>=2.10.3
netdisco==2.6.0

View File

@@ -696,7 +696,7 @@ hole==0.5.0
holidays==0.10.1
# homeassistant.components.frontend
home-assistant-frontend==20200316.0
home-assistant-frontend==20200318.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.9
@@ -1188,7 +1188,7 @@ pycfdns==0.0.1
pychannels==1.0.0
# homeassistant.components.cast
pychromecast==4.1.1
pychromecast==4.2.0
# homeassistant.components.cmus
pycmus==0.1.1
@@ -1315,7 +1315,7 @@ pyhomeworks==0.0.6
pyialarm==0.3
# homeassistant.components.icloud
pyicloud==0.9.4
pyicloud==0.9.5
# homeassistant.components.intesishome
pyintesishome==1.6
@@ -2158,7 +2158,7 @@ zengge==0.2
zeroconf==0.24.5
# homeassistant.components.zha
zha-quirks==0.0.36
zha-quirks==0.0.37
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9

View File

@@ -263,7 +263,7 @@ hole==0.5.0
holidays==0.10.1
# homeassistant.components.frontend
home-assistant-frontend==20200316.0
home-assistant-frontend==20200318.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.9
@@ -443,7 +443,7 @@ pyblackbird==0.5
pybotvac==0.0.17
# homeassistant.components.cast
pychromecast==4.1.1
pychromecast==4.2.0
# homeassistant.components.coolmaster
pycoolmasternet==0.0.4
@@ -483,7 +483,7 @@ pyheos==0.6.0
pyhomematic==0.1.65
# homeassistant.components.icloud
pyicloud==0.9.4
pyicloud==0.9.5
# homeassistant.components.ipma
pyipma==2.0.5
@@ -747,7 +747,7 @@ yahooweather==0.10
zeroconf==0.24.5
# homeassistant.components.zha
zha-quirks==0.0.36
zha-quirks==0.0.37
# homeassistant.components.zha
zigpy-cc==0.1.0

View File

@@ -156,7 +156,7 @@ def calc_allowed_references(integration: Integration) -> Set[str]:
"""Return a set of allowed references."""
allowed_references = (
ALLOWED_USED_COMPONENTS
| set(integration.manifest["dependencies"])
| set(integration.manifest.get("dependencies", []))
| set(integration.manifest.get("after_dependencies", []))
)
@@ -250,7 +250,7 @@ def validate(integrations: Dict[str, Integration], config):
validate_dependencies(integrations, integration)
# check that all referenced dependencies exist
for dep in integration.manifest["dependencies"]:
for dep in integration.manifest.get("dependencies", []):
if dep not in integrations:
integration.add_error(
"dependencies", f"Dependency {dep} does not exist"

View File

@@ -52,8 +52,8 @@ MANIFEST_SCHEMA = vol.Schema(
vol.Url(), documentation_url # pylint: disable=no-value-for-parameter
),
vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES),
vol.Required("requirements"): [str],
vol.Required("dependencies"): [str],
vol.Optional("requirements"): [str],
vol.Optional("dependencies"): [str],
vol.Optional("after_dependencies"): [str],
vol.Required("codeowners"): [str],
vol.Optional("logo"): vol.Url(), # pylint: disable=no-value-for-parameter

View File

@@ -1,5 +1,5 @@
"""Initializer helpers for HomematicIP fake server."""
from asynctest import CoroutineMock, MagicMock, Mock
from asynctest import CoroutineMock, MagicMock, Mock, patch
from homematicip.aio.auth import AsyncAuth
from homematicip.aio.connection import AsyncConnection
from homematicip.aio.home import AsyncHome
@@ -106,9 +106,10 @@ async def mock_hap_with_service_fixture(
@pytest.fixture(name="simple_mock_home")
def simple_mock_home_fixture() -> AsyncHome:
"""Return a simple AsyncHome Mock."""
return Mock(
def simple_mock_home_fixture():
"""Return a simple mocked connection."""
mock_home = Mock(
spec=AsyncHome,
name="Demo",
devices=[],
@@ -120,6 +121,27 @@ def simple_mock_home_fixture() -> AsyncHome:
connected=True,
)
with patch(
"homeassistant.components.homematicip_cloud.hap.AsyncHome",
autospec=True,
return_value=mock_home,
):
yield
@pytest.fixture(name="mock_connection_init")
def mock_connection_init_fixture():
"""Return a simple mocked connection."""
with patch(
"homeassistant.components.homematicip_cloud.hap.AsyncHome.init",
return_value=None,
), patch(
"homeassistant.components.homematicip_cloud.hap.AsyncAuth.init",
return_value=None,
):
yield
@pytest.fixture(name="simple_mock_auth")
def simple_mock_auth_fixture() -> AsyncAuth:

View File

@@ -16,12 +16,15 @@ DEFAULT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"}
IMPORT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"}
async def test_flow_works(hass):
async def test_flow_works(hass, simple_mock_home):
"""Test config flow."""
with patch(
"homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton",
return_value=False,
), patch(
"homeassistant.components.homematicip_cloud.hap.HomematicipAuth.get_auth",
return_value=True,
):
result = await hass.config_entries.flow.async_init(
HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG
@@ -137,7 +140,7 @@ async def test_init_already_configured(hass):
assert result["reason"] == "already_configured"
async def test_import_config(hass):
async def test_import_config(hass, simple_mock_home):
"""Test importing a host with an existing config file."""
with patch(
"homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton",

View File

@@ -125,14 +125,11 @@ async def test_hap_create(hass, hmip_config_entry, simple_mock_home):
hass.config.components.add(HMIPC_DOMAIN)
hap = HomematicipHAP(hass, hmip_config_entry)
assert hap
with patch(
"homeassistant.components.homematicip_cloud.hap.AsyncHome",
return_value=simple_mock_home,
), patch.object(hap, "async_connect"):
with patch.object(hap, "async_connect"):
assert await hap.async_setup()
async def test_hap_create_exception(hass, hmip_config_entry):
async def test_hap_create_exception(hass, hmip_config_entry, mock_connection_init):
"""Mock AsyncHome to execute get_hap."""
hass.config.components.add(HMIPC_DOMAIN)

View File

@@ -24,7 +24,9 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
async def test_config_with_accesspoint_passed_to_config_entry(hass):
async def test_config_with_accesspoint_passed_to_config_entry(
hass, mock_connection, simple_mock_home
):
"""Test that config for a accesspoint are loaded via config entry."""
entry_config = {
@@ -51,7 +53,9 @@ async def test_config_with_accesspoint_passed_to_config_entry(hass):
assert isinstance(hass.data[HMIPC_DOMAIN]["ABC123"], HomematicipHAP)
async def test_config_already_registered_not_passed_to_config_entry(hass):
async def test_config_already_registered_not_passed_to_config_entry(
hass, simple_mock_home
):
"""Test that an already registered accesspoint does not get imported."""
mock_config = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC123", HMIPC_NAME: "name"}
@@ -87,7 +91,9 @@ async def test_config_already_registered_not_passed_to_config_entry(hass):
assert config_entries[0].unique_id == "ABC123"
async def test_load_entry_fails_due_to_connection_error(hass, hmip_config_entry):
async def test_load_entry_fails_due_to_connection_error(
hass, hmip_config_entry, mock_connection_init
):
"""Test load entry fails due to connection error."""
hmip_config_entry.add_to_hass(hass)
@@ -101,7 +107,9 @@ async def test_load_entry_fails_due_to_connection_error(hass, hmip_config_entry)
assert hmip_config_entry.state == ENTRY_STATE_SETUP_RETRY
async def test_load_entry_fails_due_to_generic_exception(hass, hmip_config_entry):
async def test_load_entry_fails_due_to_generic_exception(
hass, hmip_config_entry, simple_mock_home
):
"""Test load entry fails due to generic exception."""
hmip_config_entry.add_to_hass(hass)

View File

@@ -373,7 +373,6 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage):
assert response["result"]["icon"] == "mdi:map"
dashboard_id = response["result"]["id"]
dashboard_path = response["result"]["url_path"]
assert "created-url-path" in hass.data[frontend.DATA_PANELS]
@@ -408,9 +407,9 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage):
)
response = await client.receive_json()
assert response["success"]
assert hass_storage[dashboard.CONFIG_STORAGE_KEY.format(dashboard_path)][
"data"
] == {"config": {"yo": "hello"}}
assert hass_storage[dashboard.CONFIG_STORAGE_KEY.format(dashboard_id)]["data"] == {
"config": {"yo": "hello"}
}
assert len(events) == 1
assert events[0].data["url_path"] == "created-url-path"

View File

@@ -160,8 +160,10 @@ async def test_webhook_handle_get_zones(hass, create_registrations, webhook_clie
assert resp.status == 200
json = await resp.json()
assert len(json) == 1
assert json[0]["entity_id"] == "zone.home"
assert len(json) == 2
zones = sorted(json, key=lambda entry: entry["entity_id"])
assert zones[0]["entity_id"] == "zone.home"
assert zones[1]["entity_id"] == "zone.test"
async def test_webhook_handle_get_config(hass, create_registrations, webhook_client):

View File

@@ -157,7 +157,7 @@ async def test_dedup_logs(hass, hass_client):
log_msg()
log = await get_error_log(hass, hass_client, 3)
assert_log(log[0], "", ["error message 2", "error message 2-2"], "ERROR")
assert log[0]["timestamp"] > log[0]["first_occured"]
assert log[0]["timestamp"] > log[0]["first_occurred"]
log_msg("2-3")
log_msg("2-4")

View File

@@ -108,7 +108,7 @@ async def test_no_clients(hass):
"""Test the update_clients function when no clients are found."""
await setup_unifi_integration(hass)
assert len(hass.states.async_all()) == 1
assert len(hass.states.async_all()) == 0
async def test_tracked_devices(hass):
@@ -123,7 +123,7 @@ async def test_tracked_devices(hass):
devices_response=[DEVICE_1, DEVICE_2],
known_wireless_clients=(CLIENT_4["mac"],),
)
assert len(hass.states.async_all()) == 7
assert len(hass.states.async_all()) == 6
client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None
@@ -184,7 +184,7 @@ async def test_controller_state_change(hass):
controller = await setup_unifi_integration(
hass, clients_response=[CLIENT_1], devices_response=[DEVICE_1],
)
assert len(hass.states.async_all()) == 3
assert len(hass.states.async_all()) == 2
# Controller unavailable
controller.async_unifi_signalling_callback(
@@ -214,7 +214,7 @@ async def test_option_track_clients(hass):
controller = await setup_unifi_integration(
hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1],
)
assert len(hass.states.async_all()) == 4
assert len(hass.states.async_all()) == 3
client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None
@@ -259,7 +259,7 @@ async def test_option_track_wired_clients(hass):
controller = await setup_unifi_integration(
hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1],
)
assert len(hass.states.async_all()) == 4
assert len(hass.states.async_all()) == 3
client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None
@@ -304,7 +304,7 @@ async def test_option_track_devices(hass):
controller = await setup_unifi_integration(
hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1],
)
assert len(hass.states.async_all()) == 4
assert len(hass.states.async_all()) == 3
client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None
@@ -349,7 +349,7 @@ async def test_option_ssid_filter(hass):
controller = await setup_unifi_integration(
hass, options={CONF_SSID_FILTER: ["ssid"]}, clients_response=[CLIENT_3],
)
assert len(hass.states.async_all()) == 2
assert len(hass.states.async_all()) == 1
# SSID filter active
client_3 = hass.states.get("device_tracker.client_3")
@@ -387,7 +387,7 @@ async def test_wireless_client_go_wired_issue(hass):
client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow())
controller = await setup_unifi_integration(hass, clients_response=[client_1_client])
assert len(hass.states.async_all()) == 2
assert len(hass.states.async_all()) == 1
client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None
@@ -460,7 +460,7 @@ async def test_restoring_client(hass):
clients_response=[CLIENT_2],
clients_all_response=[CLIENT_1],
)
assert len(hass.states.async_all()) == 3
assert len(hass.states.async_all()) == 2
device_1 = hass.states.get("device_tracker.client_1")
assert device_1 is not None
@@ -474,7 +474,7 @@ async def test_dont_track_clients(hass):
clients_response=[CLIENT_1],
devices_response=[DEVICE_1],
)
assert len(hass.states.async_all()) == 2
assert len(hass.states.async_all()) == 1
client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is None
@@ -492,7 +492,7 @@ async def test_dont_track_devices(hass):
clients_response=[CLIENT_1],
devices_response=[DEVICE_1],
)
assert len(hass.states.async_all()) == 2
assert len(hass.states.async_all()) == 1
client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None
@@ -509,7 +509,7 @@ async def test_dont_track_wired_clients(hass):
options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False},
clients_response=[CLIENT_1, CLIENT_2],
)
assert len(hass.states.async_all()) == 2
assert len(hass.states.async_all()) == 1
client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None

View File

@@ -55,7 +55,7 @@ async def test_no_clients(hass):
)
assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1
assert len(hass.states.async_all()) == 0
async def test_sensors(hass):
@@ -71,7 +71,7 @@ async def test_sensors(hass):
)
assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 5
assert len(hass.states.async_all()) == 4
wired_client_rx = hass.states.get("sensor.wired_client_name_rx")
assert wired_client_rx.state == "1234.0"

View File

@@ -209,7 +209,7 @@ async def test_no_clients(hass):
)
assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1
assert len(hass.states.async_all()) == 0
async def test_controller_not_client(hass):
@@ -222,7 +222,7 @@ async def test_controller_not_client(hass):
)
assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1
assert len(hass.states.async_all()) == 0
cloudkey = hass.states.get("switch.cloud_key")
assert cloudkey is None
@@ -240,7 +240,7 @@ async def test_not_admin(hass):
)
assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1
assert len(hass.states.async_all()) == 0
async def test_switches(hass):
@@ -258,7 +258,7 @@ async def test_switches(hass):
)
assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 4
assert len(hass.states.async_all()) == 3
switch_1 = hass.states.get("switch.poe_client_1")
assert switch_1 is not None
@@ -312,7 +312,7 @@ async def test_new_client_discovered_on_block_control(hass):
)
assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 1
assert len(hass.states.async_all()) == 0
blocked = hass.states.get("switch.block_client_1")
assert blocked is None
@@ -324,7 +324,7 @@ async def test_new_client_discovered_on_block_control(hass):
controller.api.session_handler("data")
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 2
assert len(hass.states.async_all()) == 1
blocked = hass.states.get("switch.block_client_1")
assert blocked is not None
@@ -336,7 +336,7 @@ async def test_option_block_clients(hass):
options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]},
clients_all_response=[BLOCKED, UNBLOCKED],
)
assert len(hass.states.async_all()) == 2
assert len(hass.states.async_all()) == 1
# Add a second switch
hass.config_entries.async_update_entry(
@@ -344,28 +344,28 @@ async def test_option_block_clients(hass):
options={CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]},
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 3
assert len(hass.states.async_all()) == 2
# Remove the second switch again
hass.config_entries.async_update_entry(
controller.config_entry, options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]},
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 2
assert len(hass.states.async_all()) == 1
# Enable one and remove another one
hass.config_entries.async_update_entry(
controller.config_entry, options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]},
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 2
assert len(hass.states.async_all()) == 1
# Remove one
hass.config_entries.async_update_entry(
controller.config_entry, options={CONF_BLOCK_CLIENT: []},
)
await hass.async_block_till_done()
assert len(hass.states.async_all()) == 1
assert len(hass.states.async_all()) == 0
async def test_new_client_discovered_on_poe_control(hass):
@@ -378,7 +378,7 @@ async def test_new_client_discovered_on_poe_control(hass):
)
assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 2
assert len(hass.states.async_all()) == 1
controller.api.websocket._data = {
"meta": {"message": "sta:sync"},
@@ -391,7 +391,7 @@ async def test_new_client_discovered_on_poe_control(hass):
"switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True
)
assert len(controller.mock_requests) == 5
assert len(hass.states.async_all()) == 3
assert len(hass.states.async_all()) == 2
assert controller.mock_requests[4] == {
"json": {
"port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}]
@@ -430,7 +430,7 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass):
)
assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 4
assert len(hass.states.async_all()) == 3
switch_1 = hass.states.get("switch.poe_client_1")
switch_2 = hass.states.get("switch.poe_client_2")
@@ -481,7 +481,7 @@ async def test_restoring_client(hass):
)
assert len(controller.mock_requests) == 4
assert len(hass.states.async_all()) == 3
assert len(hass.states.async_all()) == 2
device_1 = hass.states.get("switch.client_1")
assert device_1 is not None