Compare commits

...

48 Commits

Author SHA1 Message Date
Pascal Vizeli
0323aeb660 Fix machine release builds (#41879) 2020-10-15 11:52:39 +00:00
Paulus Schoutsen
58e8162d18 Merge pull request #41877 from home-assistant/rc 2020-10-15 11:35:03 +02:00
Paulus Schoutsen
8036b709e0 Bumped version to 0.116.3 2020-10-15 08:42:52 +00:00
cgtobi
e80ffe16f7 Reduce polling for Netatmo weather station sensors (#41858) 2020-10-15 08:42:27 +00:00
J. Nick Koston
99010bab18 Permit event trackers to accept an empty list of entities or domains (#41857) 2020-10-15 08:42:05 +00:00
Simone Chemelli
8d5e15df3d Shelly: fix for RGB devices (#41841) 2020-10-15 08:42:05 +00:00
Aaron Bach
efffc82414 Fix SimpliSafe re-auth flow (#41805)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2020-10-15 08:42:05 +00:00
djtimca
06b99a1ff9 Fix issues with metric conversion and single pump type for filter pumps in Omnilogic integration (#41777) 2020-10-15 08:42:05 +00:00
Pascal Vizeli
fd79c141b2 Offload dockermachines into core (#41756) 2020-10-15 08:42:05 +00:00
J. Nick Koston
b541b08d2d Ensure all template errors are caught and the websocket api reports them (#41719) 2020-10-15 08:42:00 +00:00
Bas Nijholt
42c9776c97 Fix KEF LSX becoming unavailable (#41700)
* Do not use asyncio.gather because the speaker only allows one concurrent connection

* bump to aiokef=0.2.16
2020-10-15 08:31:26 +00:00
Quentame
897340b115 Fix disabled Synology DSM camera (#41686) 2020-10-15 08:31:25 +00:00
Joakim Sørensen
e072d8b694 Add delete to hassio API (#41500) 2020-10-15 08:31:24 +00:00
Paulus Schoutsen
52d8c66a8b Merge pull request #41555 from home-assistant/rc 2020-10-09 16:46:08 +02:00
Paulus Schoutsen
a26413c989 Bumped version to 0.116.2 2020-10-09 13:44:04 +00:00
cgtobi
c9d53dec8d Fix missing weather sensors (#41540) 2020-10-09 13:42:54 +00:00
Maciej Bieniek
dae2117078 Fix description reference in PoolSense config flow (#41535) 2020-10-09 13:42:53 +00:00
Xiaonan Shen
72b95f7529 Bump yeelight to 0.5.4 (#41524) 2020-10-09 13:42:52 +00:00
J. Nick Koston
56436e6387 Restore group support to plant entities (#41519) 2020-10-09 13:42:52 +00:00
djtimca
7cdbbc248f Fix omnilogic temperature sensor with metric setting (#41509) 2020-10-09 13:42:51 +00:00
Arjan van Balken
cca2594225 Update Arris TG2492LG dependency version (#41465) 2020-10-09 13:42:50 +00:00
cgtobi
22bef1d0d3 Fix Netatmo non specified sensor types (#41517) 2020-10-09 08:37:56 +02:00
Paulus Schoutsen
b2885f6cc5 Merge pull request #41498 from home-assistant/rc 2020-10-08 19:03:12 +02:00
J. Nick Koston
3a1ee2f654 Add missing on states to media player groups (#41496) 2020-10-08 15:59:09 +00:00
Paulus Schoutsen
26ba956242 Bumped version to 0.116.1 2020-10-08 15:48:36 +00:00
Bram Kragten
90d8874825 Update frontend to 20201001.2 (#41491) 2020-10-08 15:47:57 +00:00
Guido Schmitz
22f76a9363 Fix async_unload_entry for devolo Home Control (#41488) 2020-10-08 15:47:56 +00:00
cgtobi
cc4d71f942 Bump pyatmo version to 4.1.0 (#41487) 2020-10-08 15:47:56 +00:00
Paulus Schoutsen
0a55f024a4 Downgrade Paho MQTT to 1.5.0 (#41479) 2020-10-08 15:47:55 +00:00
J. Nick Koston
60fe64d119 Ensure recorder commit can retry after encountering invalid data (#41426) 2020-10-08 15:46:40 +00:00
Khole
a3692859e9 Update Pyhiveapi Library Version (#40804)
* Update Pyhiveapi Library Version

This fixs an issue caused by a change in authentication method by hive

* Update Library Version
2020-10-08 14:01:21 +00:00
Franck Nijhof
55958bcfb7 Merge pull request #41406 from home-assistant/rc 2020-10-07 17:50:57 +02:00
Franck Nijhof
cde6400482 Bumped version to 0.116.0 2020-10-07 17:10:04 +02:00
Paulus Schoutsen
d2077acc92 Warn when using Python 3.7 (#41400) 2020-10-07 17:07:27 +02:00
Martin Hjelmare
f4991794d4 Revert zoneminder config flow (#41395) 2020-10-07 17:05:46 +02:00
Paulus Schoutsen
088fb7eff3 Reduce Somfy polling (#41389) 2020-10-07 17:00:59 +02:00
Glenn Waters
c3e679f69b Fix elkm1 changed by (#41378) 2020-10-07 13:14:03 +02:00
Alexei Chetroi
aa8e336af5 Bump up zha dependency to 0.26.0 (#41371) 2020-10-07 13:14:00 +02:00
Paulus Schoutsen
ac87c0eea2 Add additionalAttributes to Alexa discovery payload (#41370) 2020-10-07 13:13:56 +02:00
Quentame
281456b252 Set longer timeout during synology_dsm config flow (#41364) 2020-10-07 13:13:48 +02:00
Paulus Schoutsen
aefa305f77 Bumped version to 0.116.0b6 2020-10-06 20:46:27 +00:00
J. Nick Koston
a11fa832ef Resolve memory leak in recorder (#41349)
Avoids a build up of the InstanceState.
2020-10-06 20:46:20 +00:00
Franck Nijhof
0470142701 Fix TTS ID3 Tag capability check (#41343) 2020-10-06 20:46:19 +00:00
Steven Looman
3c22834751 Don't set upnp config_entry.unique_id from setup entry (#40988)
* Don't set config_entry.unique_id from setup entry. Fixes #40168

* Ensure entry has a unique_id

* Add test test_flow_import_incomplete

* Add test test_flow_import_duplicate

* Re-add testing import_info

* Simplify import flow

* Remove not needed line

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2020-10-06 20:46:19 +00:00
Paulus Schoutsen
570d1e7d8f Bumped version to 0.116.0b5 2020-10-06 07:55:25 +00:00
J. Nick Koston
23be039b7f Prevent collecting states already referenced by domain or all (#41308)
The template engine would collect all the states in
a domain or all states while iterating even though
they were already included in all or the domain

This lead to the rate limit not being applied to
templates that iterated all states that also
accessed a collectable property because the engine
incorrectly believed they were specifically
referenced.
2020-10-06 07:55:18 +00:00
Bram Kragten
859632d636 Exclude media_dirs from YAML config check (#41299) 2020-10-06 07:55:18 +00:00
Andrew Sayre
874cbf808e Update pysmartthings (#41294) 2020-10-06 07:55:17 +00:00
84 changed files with 1423 additions and 1423 deletions

View File

@@ -1050,6 +1050,7 @@ omit =
homeassistant/components/zhong_hong/climate.py
homeassistant/components/xbee/*
homeassistant/components/ziggo_mediabox_xl/media_player.py
homeassistant/components/zoneminder/*
homeassistant/components/supla/*
homeassistant/components/zwave/util.py
homeassistant/components/ozw/__init__.py

View File

@@ -512,7 +512,7 @@ homeassistant/components/zerproc/* @emlove
homeassistant/components/zha/* @dmulcahey @adminiuga
homeassistant/components/zodiac/* @JulienTant
homeassistant/components/zone/* @home-assistant/core
homeassistant/components/zoneminder/* @rohankapoorcom @vangorra
homeassistant/components/zoneminder/* @rohankapoorcom
homeassistant/components/zwave/* @home-assistant/z-wave
# Individual files

View File

@@ -115,10 +115,10 @@ stages:
docker run --rm --privileged \
-v ~/.docker:/root/.docker \
-v /run/docker.sock:/run/docker.sock:rw \
-v $(pwd):/data:ro \
homeassistant/amd64-builder:$(versionBuilder) \
--homeassistant-machine "$(homeassistantRelease)=$(buildMachine)" \
-r https://github.com/home-assistant/hassio-homeassistant \
-t machine --docker-hub homeassistant
-t /data/machine --docker-hub homeassistant
displayName: 'Build Release'
- stage: 'Publish'

View File

@@ -33,6 +33,7 @@ from homeassistant.const import (
CONF_NAME,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
__version__,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import network
@@ -286,6 +287,12 @@ class AlexaEntity:
"friendlyName": self.friendly_name(),
"description": self.description(),
"manufacturerName": "Home Assistant",
"additionalAttributes": {
"manufacturer": "Home Assistant",
"model": self.entity.domain,
"softwareVersion": __version__,
"customIdentifier": self.entity_id,
},
}
locale = self.config.locale

View File

@@ -3,7 +3,7 @@
"name": "Arris TG2492LG",
"documentation": "https://www.home-assistant.io/integrations/arris_tg2492lg",
"requirements": [
"arris-tg2492lg==1.0.0"
"arris-tg2492lg==1.1.0"
],
"codeowners": [
"@vanbalken"

View File

@@ -1,4 +1,5 @@
"""The devolo_home_control integration."""
import asyncio
from functools import partial
from devolo_home_control_api.homecontrol import HomeControl
@@ -71,8 +72,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
unload = await hass.config_entries.async_forward_entry_unload(
config_entry, "switch"
unload = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(config_entry, platform)
for platform in PLATFORMS
]
)
)
await hass.async_add_executor_job(

View File

@@ -155,18 +155,16 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity):
self.async_write_ha_state()
def _watch_area(self, area, changeset):
if not changeset.get("log_event"):
last_log = changeset.get("last_log")
if not last_log:
return
# user_number only set for arm/disarm logs
if not last_log.get("user_number"):
return
self._changed_by_keypad = None
self._changed_by_id = area.log_number
self._changed_by = username(self._elk, area.log_number - 1)
self._changed_by_time = "%04d-%02d-%02dT%02d:%02d" % (
area.log_year,
area.log_month,
area.log_day,
area.log_hour,
area.log_minute,
)
self._changed_by_id = last_log["user_number"]
self._changed_by = username(self._elk, self._changed_by_id - 1)
self._changed_by_time = last_log["timestamp"]
self.async_write_ha_state()
@property

View File

@@ -2,7 +2,7 @@
"domain": "elkm1",
"name": "Elk-M1 Control",
"documentation": "https://www.home-assistant.io/integrations/elkm1",
"requirements": ["elkm1-lib==0.7.19"],
"requirements": ["elkm1-lib==0.8.0"],
"codeowners": ["@gwww", "@bdraco"],
"config_flow": true
}

View File

@@ -2,7 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20201001.1"],
"requirements": ["home-assistant-frontend==20201001.2"],
"dependencies": [
"api",
"auth",

View File

@@ -65,6 +65,7 @@ class HassIOView(HomeAssistantView):
return await self._command_proxy(path, request)
delete = _handle
get = _handle
post = _handle

View File

@@ -2,6 +2,6 @@
"domain": "hive",
"name": "Hive",
"documentation": "https://www.home-assistant.io/integrations/hive",
"requirements": ["pyhiveapi==0.2.20.1"],
"requirements": ["pyhiveapi==0.2.20.2"],
"codeowners": ["@Rendili", "@KJonline"]
}

View File

@@ -3,5 +3,5 @@
"name": "KEF",
"documentation": "https://www.home-assistant.io/integrations/kef",
"codeowners": ["@basnijholt"],
"requirements": ["aiokef==0.2.13", "getmac==0.8.2"]
"requirements": ["aiokef==0.2.16", "getmac==0.8.2"]
}

View File

@@ -1,6 +1,5 @@
"""Platform for the KEF Wireless Speakers."""
import asyncio
from datetime import timedelta
from functools import partial
import ipaddress
@@ -164,7 +163,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
dtype = type(options[0]) # int or float
platform.async_register_entity_service(
name,
{vol.Required(option): vol.All(vol.Coerce(dtype), vol.In(options))},
{
vol.Required(option): vol.All(
vol.Coerce(float), vol.Coerce(dtype), vol.In(options)
)
},
f"set_{which}",
)
@@ -365,17 +368,16 @@ class KefMediaPlayer(MediaPlayerEntity):
# The LSX is able to respond when off the LS50 has to be on.
return
(mode, *rest) = await asyncio.gather(
self._speaker.get_mode(),
self._speaker.get_desk_db(),
self._speaker.get_wall_db(),
self._speaker.get_treble_db(),
self._speaker.get_high_hz(),
self._speaker.get_low_hz(),
self._speaker.get_sub_db(),
mode = await self._speaker.get_mode()
self._dsp = dict(
desk_db=await self._speaker.get_desk_db(),
wall_db=await self._speaker.get_wall_db(),
treble_db=await self._speaker.get_treble_db(),
high_hz=await self._speaker.get_high_hz(),
low_hz=await self._speaker.get_low_hz(),
sub_db=await self._speaker.get_sub_db(),
**mode._asdict(),
)
keys = ["desk_db", "wall_db", "treble_db", "high_hz", "low_hz", "sub_db"]
self._dsp = dict(zip(keys, rest), **mode._asdict())
async def async_added_to_hass(self):
"""Subscribe to DSP updates."""

View File

@@ -2,16 +2,22 @@
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OFF
from homeassistant.const import (
STATE_IDLE,
STATE_OFF,
STATE_ON,
STATE_PAUSED,
STATE_PLAYING,
)
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
from . import STATE_IDLE, STATE_PLAYING
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_PLAYING, STATE_IDLE}, STATE_OFF)
registry.on_off_states(
{STATE_ON, STATE_PAUSED, STATE_PLAYING, STATE_IDLE}, STATE_OFF
)

View File

@@ -3,7 +3,7 @@
"name": "MQTT",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mqtt",
"requirements": ["paho-mqtt==1.5.1"],
"requirements": ["paho-mqtt==1.5.0"],
"dependencies": ["http"],
"codeowners": ["@home-assistant/core", "@emontnemery"]
}

View File

@@ -41,7 +41,7 @@ DEFAULT_INTERVALS = {
HOMEDATA_DATA_CLASS_NAME: 900,
HOMESTATUS_DATA_CLASS_NAME: 300,
CAMERA_DATA_CLASS_NAME: 900,
WEATHERSTATION_DATA_CLASS_NAME: 300,
WEATHERSTATION_DATA_CLASS_NAME: 600,
HOMECOACH_DATA_CLASS_NAME: 300,
PUBLICDATA_DATA_CLASS_NAME: 600,
}

View File

@@ -3,7 +3,7 @@
"name": "Netatmo",
"documentation": "https://www.home-assistant.io/integrations/netatmo",
"requirements": [
"pyatmo==4.0.0"
"pyatmo==4.1.0"
],
"after_dependencies": [
"cloud",

View File

@@ -136,6 +136,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
conditions = [
c.lower()
for c in data_class.get_monitored_conditions(module_id=module["_id"])
if c.lower() in SENSOR_TYPES
]
for condition in conditions:
if f"{condition}_value" in SENSOR_TYPES:

View File

@@ -119,7 +119,7 @@ class OmniLogicTemperatureSensor(OmnilogicSensor):
state = sensor_data
if self._unit_type == "Metric":
hayward_state = round((hayward_state - 32) * 5 / 9, 1)
hayward_state = round((int(hayward_state) - 32) * 5 / 9, 1)
hayward_unit_of_measure = TEMP_CELSIUS
if int(sensor_data) == -1:
@@ -175,7 +175,7 @@ class OmniLogicSaltLevelSensor(OmnilogicSensor):
unit_of_measurement = self._unit
if self._unit_type == "Metric":
salt_return = round(salt_return / 1000, 2)
salt_return = round(int(salt_return) / 1000, 2)
unit_of_measurement = f"{MASS_GRAMS}/{VOLUME_LITERS}"
self._unit = unit_of_measurement
@@ -279,7 +279,7 @@ SENSOR_TYPES = {
"icon": "mdi:speedometer",
"unit": PERCENTAGE,
"guard_condition": [
{"Type": "FMT_SINGLE_SPEED"},
{"Filter-Type": "FMT_SINGLE_SPEED"},
],
},
],

View File

@@ -0,0 +1,15 @@
"""Describe group states."""
from homeassistant.components.group import GroupIntegrationRegistry
from homeassistant.const import STATE_OK, STATE_PROBLEM
from homeassistant.core import callback
from homeassistant.helpers.typing import HomeAssistantType
@callback
def async_describe_on_off_states(
hass: HomeAssistantType, registry: GroupIntegrationRegistry
) -> None:
"""Describe group on off states."""
registry.on_off_states({STATE_PROBLEM}, STATE_OK)

View File

@@ -3,7 +3,7 @@
"step": {
"user": {
"title": "PoolSense",
"description": "[%key:common::config_flow::description%]",
"description": "[%key:common::config_flow::description::confirm_setup%]",
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"

View File

@@ -240,6 +240,7 @@ class Recorder(threading.Thread):
self._timechanges_seen = 0
self._keepalive_count = 0
self._old_states = {}
self._pending_expunge = []
self.event_session = None
self.get_session = None
self._completed_database_setup = False
@@ -403,6 +404,7 @@ class Recorder(threading.Thread):
self.event_session.add(dbstate)
if has_new_state:
self._old_states[dbstate.entity_id] = dbstate
self._pending_expunge.append(dbstate)
except (TypeError, ValueError):
_LOGGER.warning(
"State is not JSON serializable: %s",
@@ -488,6 +490,13 @@ class Recorder(threading.Thread):
def _commit_event_session(self):
try:
self.event_session.flush()
for dbstate in self._pending_expunge:
# Expunge the state so its not expired
# until we use it later for dbstate.old_state
if dbstate in self.event_session:
self.event_session.expunge(dbstate)
self._pending_expunge = []
self.event_session.commit()
except Exception as err:
_LOGGER.error("Error executing query: %s", err)
@@ -573,9 +582,7 @@ class Recorder(threading.Thread):
sqlalchemy_event.listen(self.engine, "connect", setup_recorder_connection)
Base.metadata.create_all(self.engine)
self.get_session = scoped_session(
sessionmaker(bind=self.engine, expire_on_commit=False)
)
self.get_session = scoped_session(sessionmaker(bind=self.engine))
def _close_connection(self):
"""Close the connection."""

View File

@@ -28,7 +28,7 @@ def shelly_naming(self, block, entity_type: str):
return f"{entity_name} {self.description.name}"
channels = 0
mode = "relays"
mode = block.type + "s"
if "num_outputs" in self.wrapper.device.shelly:
channels = self.wrapper.device.shelly["num_outputs"]
if (
@@ -38,7 +38,6 @@ def shelly_naming(self, block, entity_type: str):
channels = 1
if block.type == "emeter" and "num_emeters" in self.wrapper.device.shelly:
channels = self.wrapper.device.shelly["num_emeters"]
mode = "emeters"
if channels > 1 and block.type != "device":
# Shelly EM (SHEM) with firmware v1.8.1 doesn't have "name" key; will be fixed in next firmware release
if "name" in self.wrapper.device.settings[mode][int(block.channel)]:

View File

@@ -16,7 +16,7 @@ from simplipy.websocket import (
)
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH
from homeassistant.const import (
ATTR_CODE,
CONF_CODE,
@@ -365,8 +365,7 @@ async def async_unload_entry(hass, entry):
async def async_update_options(hass, config_entry):
"""Handle an options update."""
simplisafe = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
simplisafe.options = config_entry.options
await hass.config_entries.async_reload(config_entry.entry_id)
class SimpliSafeWebsocket:
@@ -530,17 +529,26 @@ class SimpliSafe:
for result in results:
if isinstance(result, InvalidCredentialsError):
if self._emergency_refresh_token_used:
LOGGER.error(
"Token disconnected or invalid. Please re-auth the "
"SimpliSafe integration in HASS"
)
self._hass.async_create_task(
self._hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "reauth"},
data=self._config_entry.data,
matching_flows = [
flow
for flow in self._hass.config_entries.flow.async_progress()
if flow["context"].get("source") == SOURCE_REAUTH
and flow["context"].get("unique_id")
== self._config_entry.unique_id
]
if not matching_flows:
self._hass.async_create_task(
self._hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": SOURCE_REAUTH,
"unique_id": self._config_entry.unique_id,
},
data=self._config_entry.data,
)
)
)
return
LOGGER.warning("SimpliSafe cloud error; trying stored refresh token")

View File

@@ -3,7 +3,7 @@
"name": "SmartThings",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/smartthings",
"requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.3"],
"requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.4"],
"dependencies": ["webhook"],
"after_dependencies": ["cloud"],
"codeowners": ["@andrewsayre"]

View File

@@ -28,7 +28,7 @@ DEVICES = "devices"
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=30)
SCAN_INTERVAL = timedelta(minutes=1)
CONF_OPTIMISTIC = "optimistic"

View File

@@ -2,12 +2,13 @@
from typing import Dict
from synology_dsm.api.surveillance_station import SynoSurveillanceStation
from synology_dsm.api.surveillance_station.camera import SynoCamera
from homeassistant.components.camera import SUPPORT_STREAM, Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from . import SynologyDSMEntity
from . import SynoApi, SynologyDSMEntity
from .const import (
DOMAIN,
ENTITY_CLASS,
@@ -40,7 +41,7 @@ async def async_setup_entry(
class SynoDSMCamera(SynologyDSMEntity, Camera):
"""Representation a Synology camera."""
def __init__(self, api, camera):
def __init__(self, api: SynoApi, camera: SynoCamera):
"""Initialize a Synology camera."""
super().__init__(
api,
@@ -69,6 +70,11 @@ class SynoDSMCamera(SynologyDSMEntity, Camera):
),
}
@property
def available(self) -> bool:
"""Return the availability of the camera."""
return self._camera.is_enabled
@property
def supported_features(self) -> int:
"""Return supported features of this camera."""
@@ -86,10 +92,14 @@ class SynoDSMCamera(SynologyDSMEntity, Camera):
def camera_image(self) -> bytes:
"""Return bytes of camera image."""
if not self.available:
return None
return self._api.surveillance_station.get_camera_image(self._camera.id)
async def stream_source(self) -> str:
"""Return the source of the stream."""
if not self.available:
return None
return self._camera.live_view.rtsp
def enable_motion_detection(self):

View File

@@ -126,7 +126,7 @@ class SynologyDSMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
else:
port = DEFAULT_PORT
api = SynologyDSM(host, port, username, password, use_ssl)
api = SynologyDSM(host, port, username, password, use_ssl, timeout=30)
try:
serial = await self.hass.async_add_executor_job(

View File

@@ -11,7 +11,7 @@ from typing import Dict, Optional
from aiohttp import web
import mutagen
from mutagen.id3 import ID3, TextFrame as ID3Text
from mutagen.id3 import ID3FileType, TextFrame as ID3Text
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
@@ -468,7 +468,7 @@ class SpeechManager:
try:
tts_file = mutagen.File(data_bytes)
if tts_file is not None:
if isinstance(tts_file.tags, ID3):
if isinstance(tts_file, ID3FileType):
tts_file["artist"] = ID3Text(encoding=3, text=artist)
tts_file["album"] = ID3Text(encoding=3, text=album)
tts_file["title"] = ID3Text(encoding=3, text=message)

View File

@@ -56,7 +56,9 @@ async def async_discover_and_construct(
filtered = [di for di in discovery_infos if di[DISCOVERY_ST] == st]
if not filtered:
_LOGGER.warning(
'Wanted UPnP/IGD device with UDN "%s" not found, aborting', udn
'Wanted UPnP/IGD device with UDN/ST "%s"/"%s" not found, aborting',
udn,
st,
)
return None
@@ -104,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
"""Set up UPnP/IGD device from a config entry."""
_LOGGER.debug("async_setup_entry, config_entry: %s", config_entry.data)
# discover and construct
# Discover and construct.
udn = config_entry.data.get(CONFIG_ENTRY_UDN)
st = config_entry.data.get(CONFIG_ENTRY_ST) # pylint: disable=invalid-name
try:
@@ -116,11 +118,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
_LOGGER.info("Unable to create UPnP/IGD, aborting")
raise ConfigEntryNotReady
# Save device
# Save device.
hass.data[DOMAIN][DOMAIN_DEVICES][device.udn] = device
# Ensure entry has proper unique_id.
if config_entry.unique_id != device.unique_id:
# Ensure entry has a unique_id.
if not config_entry.unique_id:
hass.config_entries.async_update_entry(
entry=config_entry,
unique_id=device.unique_id,

View File

@@ -104,19 +104,10 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""
_LOGGER.debug("async_step_import: import_info: %s", import_info)
if import_info is None:
# Landed here via configuration.yaml entry.
# Any device already added, then abort.
if self._async_current_entries():
_LOGGER.debug("aborting, already configured")
return self.async_abort(reason="already_configured")
# Test if import_info isn't already configured.
if import_info is not None and any(
import_info["udn"] == entry.data[CONFIG_ENTRY_UDN]
and import_info["st"] == entry.data[CONFIG_ENTRY_ST]
for entry in self._async_current_entries()
):
# Landed here via configuration.yaml entry.
# Any device already added, then abort.
if self._async_current_entries():
_LOGGER.debug("Already configured, aborting")
return self.async_abort(reason="already_configured")
# Discover devices.
@@ -127,8 +118,17 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.info("No UPnP devices discovered, aborting")
return self.async_abort(reason="no_devices_found")
discovery = self._discoveries[0]
return await self._async_create_entry_from_discovery(discovery)
# Ensure complete discovery.
discovery_info = self._discoveries[0]
if DISCOVERY_USN not in discovery_info:
_LOGGER.debug("Incomplete discovery, ignoring")
return self.async_abort(reason="incomplete_discovery")
# Ensure not already configuring/configured.
usn = discovery_info[DISCOVERY_USN]
await self.async_set_unique_id(usn)
return await self._async_create_entry_from_discovery(discovery_info)
async def async_step_ssdp(self, discovery_info: Mapping):
"""Handle a discovered UPnP/IGD device.
@@ -191,7 +191,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
):
"""Create an entry from discovery."""
_LOGGER.debug(
"_async_create_entry_from_data: discovery: %s",
"_async_create_entry_from_discovery: discovery: %s",
discovery,
)
# Get name from device, if not found already.

View File

@@ -257,13 +257,20 @@ async def handle_render_template(hass, connection, msg):
timeout = msg.get("timeout")
info = None
if timeout and await template.async_render_will_timeout(timeout):
connection.send_error(
msg["id"],
const.ERR_TEMPLATE_ERROR,
f"Exceeded maximum execution time of {timeout}s",
)
return
if timeout:
try:
timed_out = await template.async_render_will_timeout(timeout)
except TemplateError as ex:
connection.send_error(msg["id"], const.ERR_TEMPLATE_ERROR, str(ex))
return
if timed_out:
connection.send_error(
msg["id"],
const.ERR_TEMPLATE_ERROR,
f"Exceeded maximum execution time of {timeout}s",
)
return
@callback
def _template_listener(event, updates):

View File

@@ -137,7 +137,7 @@ EFFECTS_MAP = {
EFFECT_POLICE2: yee_transitions.police2,
EFFECT_CHRISTMAS: yee_transitions.christmas,
EFFECT_RGB: yee_transitions.rgb,
EFFECT_RANDOM_LOOP: yee_transitions.randomloop,
EFFECT_RANDOM_LOOP: yee_transitions.random_loop,
EFFECT_LSD: yee_transitions.lsd,
EFFECT_SLOWDOWN: yee_transitions.slowdown,
}
@@ -661,7 +661,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
elif effect in EFFECTS_MAP:
flow = Flow(count=0, transitions=EFFECTS_MAP[effect]())
elif effect == EFFECT_FAST_RANDOM_LOOP:
flow = Flow(count=0, transitions=yee_transitions.randomloop(duration=250))
flow = Flow(count=0, transitions=yee_transitions.random_loop(duration=250))
elif effect == EFFECT_WHATSAPP:
flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102))
elif effect == EFFECT_FACEBOOK:

View File

@@ -3,7 +3,7 @@
"name": "Yeelight",
"documentation": "https://www.home-assistant.io/integrations/yeelight",
"requirements": [
"yeelight==0.5.3"
"yeelight==0.5.4"
],
"codeowners": [
"@rytilahti",

View File

@@ -9,7 +9,7 @@
"zha-quirks==0.0.45",
"zigpy-cc==0.5.2",
"zigpy-deconz==0.10.0",
"zigpy==0.25.0",
"zigpy==0.26.0",
"zigpy-xbee==0.13.0",
"zigpy-zigate==0.6.2",
"zigpy-znp==0.2.1"

View File

@@ -2,169 +2,97 @@
import logging
import voluptuous as vol
from zoneminder.zm import ZoneMinder
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
import homeassistant.config_entries as config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ID,
ATTR_NAME,
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
CONF_PLATFORM,
CONF_SOURCE,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from . import const
from .common import (
ClientAvailabilityResult,
async_test_client_availability,
create_client_from_config,
del_client_from_data,
get_client_from_data,
is_client_in_data,
set_client_to_data,
set_platform_configs,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
_LOGGER = logging.getLogger(__name__)
PLATFORM_DOMAINS = tuple(
[BINARY_SENSOR_DOMAIN, CAMERA_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN]
)
CONF_PATH_ZMS = "path_zms"
DEFAULT_PATH = "/zm/"
DEFAULT_PATH_ZMS = "/zm/cgi-bin/nph-zms"
DEFAULT_SSL = False
DEFAULT_TIMEOUT = 10
DEFAULT_VERIFY_SSL = True
DOMAIN = "zoneminder"
HOST_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PATH, default=const.DEFAULT_PATH): cv.string,
vol.Optional(const.CONF_PATH_ZMS, default=const.DEFAULT_PATH_ZMS): cv.string,
vol.Optional(CONF_SSL, default=const.DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
vol.Optional(CONF_PATH_ZMS, default=DEFAULT_PATH_ZMS): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_VERIFY_SSL, default=const.DEFAULT_VERIFY_SSL): cv.boolean,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
}
)
CONFIG_SCHEMA = vol.All(
cv.deprecated(const.DOMAIN, invalidation_version="0.118"),
vol.Schema(
{const.DOMAIN: vol.All(cv.ensure_list, [HOST_CONFIG_SCHEMA])},
extra=vol.ALLOW_EXTRA,
),
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [HOST_CONFIG_SCHEMA])}, extra=vol.ALLOW_EXTRA
)
SERVICE_SET_RUN_STATE = "set_run_state"
SET_RUN_STATE_SCHEMA = vol.Schema(
{vol.Required(ATTR_ID): cv.string, vol.Required(ATTR_NAME): cv.string}
)
async def async_setup(hass: HomeAssistant, base_config: dict):
def setup(hass, config):
"""Set up the ZoneMinder component."""
# Collect the platform specific configs. It's necessary to collect these configs
# here instead of the platform's setup_platform function because the invocation order
# of setup_platform and async_setup_entry is not consistent.
set_platform_configs(
hass,
SENSOR_DOMAIN,
[
platform_config
for platform_config in base_config.get(SENSOR_DOMAIN, [])
if platform_config[CONF_PLATFORM] == const.DOMAIN
],
)
set_platform_configs(
hass,
SWITCH_DOMAIN,
[
platform_config
for platform_config in base_config.get(SWITCH_DOMAIN, [])
if platform_config[CONF_PLATFORM] == const.DOMAIN
],
hass.data[DOMAIN] = {}
success = True
for conf in config[DOMAIN]:
protocol = "https" if conf[CONF_SSL] else "http"
host_name = conf[CONF_HOST]
server_origin = f"{protocol}://{host_name}"
zm_client = ZoneMinder(
server_origin,
conf.get(CONF_USERNAME),
conf.get(CONF_PASSWORD),
conf.get(CONF_PATH),
conf.get(CONF_PATH_ZMS),
conf.get(CONF_VERIFY_SSL),
)
hass.data[DOMAIN][host_name] = zm_client
success = zm_client.login() and success
def set_active_state(call):
"""Set the ZoneMinder run state to the given state name."""
zm_id = call.data[ATTR_ID]
state_name = call.data[ATTR_NAME]
if zm_id not in hass.data[DOMAIN]:
_LOGGER.error("Invalid ZoneMinder host provided: %s", zm_id)
if not hass.data[DOMAIN][zm_id].set_active_state(state_name):
_LOGGER.error(
"Unable to change ZoneMinder state. Host: %s, state: %s",
zm_id,
state_name,
)
hass.services.register(
DOMAIN, SERVICE_SET_RUN_STATE, set_active_state, schema=SET_RUN_STATE_SCHEMA
)
config = base_config.get(const.DOMAIN)
hass.async_create_task(
async_load_platform(hass, "binary_sensor", DOMAIN, {}, config)
)
if not config:
return True
for config_item in config:
hass.async_create_task(
hass.config_entries.flow.async_init(
const.DOMAIN,
context={CONF_SOURCE: config_entries.SOURCE_IMPORT},
data=config_item,
)
)
return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up Zoneminder config entry."""
zm_client = create_client_from_config(config_entry.data)
result = await async_test_client_availability(hass, zm_client)
if result != ClientAvailabilityResult.AVAILABLE:
raise ConfigEntryNotReady
set_client_to_data(hass, config_entry.unique_id, zm_client)
for platform_domain in PLATFORM_DOMAINS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform_domain)
)
if not hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE):
@callback
def set_active_state(call):
"""Set the ZoneMinder run state to the given state name."""
zm_id = call.data[ATTR_ID]
state_name = call.data[ATTR_NAME]
if not is_client_in_data(hass, zm_id):
_LOGGER.error("Invalid ZoneMinder host provided: %s", zm_id)
return
if not get_client_from_data(hass, zm_id).set_active_state(state_name):
_LOGGER.error(
"Unable to change ZoneMinder state. Host: %s, state: %s",
zm_id,
state_name,
)
hass.services.async_register(
const.DOMAIN,
const.SERVICE_SET_RUN_STATE,
set_active_state,
schema=SET_RUN_STATE_SCHEMA,
)
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload Zoneminder config entry."""
for platform_domain in PLATFORM_DOMAINS:
hass.async_create_task(
hass.config_entries.async_forward_entry_unload(
config_entry, platform_domain
)
)
# If this is the last config to exist, remove the service too.
if len(hass.config_entries.async_entries(const.DOMAIN)) <= 1:
hass.services.async_remove(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
del_client_from_data(hass, config_entry.unique_id)
return True
return success

View File

@@ -1,43 +1,29 @@
"""Support for ZoneMinder binary sensors."""
from typing import Callable, List, Optional
from zoneminder.zm import ZoneMinder
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_CONNECTIVITY,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from .common import get_client_from_data
from . import DOMAIN as ZONEMINDER_DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: Callable[[List[Entity], Optional[bool]], None],
) -> None:
"""Set up the sensor config entry."""
zm_client = get_client_from_data(hass, config_entry.unique_id)
async_add_entities([ZMAvailabilitySensor(zm_client, config_entry)])
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder binary sensor platform."""
sensors = []
for host_name, zm_client in hass.data[ZONEMINDER_DOMAIN].items():
sensors.append(ZMAvailabilitySensor(host_name, zm_client))
add_entities(sensors)
return True
class ZMAvailabilitySensor(BinarySensorEntity):
"""Representation of the availability of ZoneMinder as a binary sensor."""
def __init__(self, client: ZoneMinder, config_entry: ConfigEntry):
def __init__(self, host_name, client):
"""Initialize availability sensor."""
self._state = None
self._name = config_entry.unique_id
self._name = host_name
self._client = client
self._config_entry = config_entry
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return f"{self._config_entry.unique_id}_availability"
@property
def name(self):

View File

@@ -1,8 +1,5 @@
"""Support for ZoneMinder camera streaming."""
import logging
from typing import Callable, List, Optional
from zoneminder.monitor import Monitor
from homeassistant.components.mjpeg.camera import (
CONF_MJPEG_URL,
@@ -10,12 +7,9 @@ from homeassistant.components.mjpeg.camera import (
MjpegCamera,
filter_urllib3_logging,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_VERIFY_SSL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from .common import get_client_from_data
from . import DOMAIN as ZONEMINDER_DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -23,28 +17,23 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder cameras."""
filter_urllib3_logging()
cameras = []
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
monitors = zm_client.get_monitors()
if not monitors:
_LOGGER.warning("Could not fetch monitors from ZoneMinder host: %s")
return
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: Callable[[List[Entity], Optional[bool]], None],
) -> None:
"""Set up the sensor config entry."""
zm_client = get_client_from_data(hass, config_entry.unique_id)
async_add_entities(
[
ZoneMinderCamera(monitor, zm_client.verify_ssl, config_entry)
for monitor in await hass.async_add_job(zm_client.get_monitors)
]
)
for monitor in monitors:
_LOGGER.info("Initializing camera %s", monitor.id)
cameras.append(ZoneMinderCamera(monitor, zm_client.verify_ssl))
add_entities(cameras)
class ZoneMinderCamera(MjpegCamera):
"""Representation of a ZoneMinder Monitor Stream."""
def __init__(self, monitor: Monitor, verify_ssl: bool, config_entry: ConfigEntry):
def __init__(self, monitor, verify_ssl):
"""Initialize as a subclass of MjpegCamera."""
device_info = {
CONF_NAME: monitor.name,
@@ -56,12 +45,6 @@ class ZoneMinderCamera(MjpegCamera):
self._is_recording = None
self._is_available = None
self._monitor = monitor
self._config_entry = config_entry
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return f"{self._config_entry.unique_id}_{self._monitor.id}_camera"
@property
def should_poll(self):

View File

@@ -1,110 +0,0 @@
"""Common code for the ZoneMinder component."""
from enum import Enum
from typing import List
import requests
from zoneminder.zm import ZoneMinder
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from . import const
def prime_domain_data(hass: HomeAssistant) -> None:
"""Prime the data structures."""
hass.data.setdefault(const.DOMAIN, {})
def prime_platform_configs(hass: HomeAssistant, domain: str) -> None:
"""Prime the data structures."""
prime_domain_data(hass)
hass.data[const.DOMAIN].setdefault(const.PLATFORM_CONFIGS, {})
hass.data[const.DOMAIN][const.PLATFORM_CONFIGS].setdefault(domain, [])
def set_platform_configs(hass: HomeAssistant, domain: str, configs: List[dict]) -> None:
"""Set platform configs."""
prime_platform_configs(hass, domain)
hass.data[const.DOMAIN][const.PLATFORM_CONFIGS][domain] = configs
def get_platform_configs(hass: HomeAssistant, domain: str) -> List[dict]:
"""Get platform configs."""
prime_platform_configs(hass, domain)
return hass.data[const.DOMAIN][const.PLATFORM_CONFIGS][domain]
def prime_config_data(hass: HomeAssistant, unique_id: str) -> None:
"""Prime the data structures."""
prime_domain_data(hass)
hass.data[const.DOMAIN].setdefault(const.CONFIG_DATA, {})
hass.data[const.DOMAIN][const.CONFIG_DATA].setdefault(unique_id, {})
def set_client_to_data(hass: HomeAssistant, unique_id: str, client: ZoneMinder) -> None:
"""Put a ZoneMinder client in the Home Assistant data."""
prime_config_data(hass, unique_id)
hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id][const.API_CLIENT] = client
def is_client_in_data(hass: HomeAssistant, unique_id: str) -> bool:
"""Check if ZoneMinder client is in the Home Assistant data."""
prime_config_data(hass, unique_id)
return const.API_CLIENT in hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id]
def get_client_from_data(hass: HomeAssistant, unique_id: str) -> ZoneMinder:
"""Get a ZoneMinder client from the Home Assistant data."""
prime_config_data(hass, unique_id)
return hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id][const.API_CLIENT]
def del_client_from_data(hass: HomeAssistant, unique_id: str) -> None:
"""Delete a ZoneMinder client from the Home Assistant data."""
prime_config_data(hass, unique_id)
del hass.data[const.DOMAIN][const.CONFIG_DATA][unique_id][const.API_CLIENT]
def create_client_from_config(conf: dict) -> ZoneMinder:
"""Create a new ZoneMinder client from a config."""
protocol = "https" if conf[CONF_SSL] else "http"
host_name = conf[CONF_HOST]
server_origin = f"{protocol}://{host_name}"
return ZoneMinder(
server_origin,
conf.get(CONF_USERNAME),
conf.get(CONF_PASSWORD),
conf.get(CONF_PATH),
conf.get(const.CONF_PATH_ZMS),
conf.get(CONF_VERIFY_SSL),
)
class ClientAvailabilityResult(Enum):
"""Client availability test result."""
AVAILABLE = "available"
ERROR_AUTH_FAIL = "auth_fail"
ERROR_CONNECTION_ERROR = "connection_error"
async def async_test_client_availability(
hass: HomeAssistant, client: ZoneMinder
) -> ClientAvailabilityResult:
"""Test the availability of a ZoneMinder client."""
try:
if await hass.async_add_job(client.login):
return ClientAvailabilityResult.AVAILABLE
return ClientAvailabilityResult.ERROR_AUTH_FAIL
except requests.exceptions.ConnectionError:
return ClientAvailabilityResult.ERROR_CONNECTION_ERROR

View File

@@ -1,99 +0,0 @@
"""ZoneMinder config flow."""
from urllib.parse import urlparse
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
CONF_SOURCE,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from .common import (
ClientAvailabilityResult,
async_test_client_availability,
create_client_from_config,
)
from .const import (
CONF_PATH_ZMS,
DEFAULT_PATH,
DEFAULT_PATH_ZMS,
DEFAULT_SSL,
DEFAULT_VERIFY_SSL,
)
from .const import DOMAIN # pylint: disable=unused-import
class ZoneminderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Flow handler for zoneminder integration."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
async def async_step_import(self, config: dict):
"""Handle a flow initialized by import."""
return await self.async_step_finish(
{**config, **{CONF_SOURCE: config_entries.SOURCE_IMPORT}}
)
async def async_step_user(self, user_input: dict = None):
"""Handle user step."""
user_input = user_input or {}
errors = {}
if user_input:
zm_client = create_client_from_config(user_input)
result = await async_test_client_availability(self.hass, zm_client)
if result == ClientAvailabilityResult.AVAILABLE:
return await self.async_step_finish(user_input)
errors["base"] = result.value
return self.async_show_form(
step_id=config_entries.SOURCE_USER,
data_schema=vol.Schema(
{
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str,
vol.Optional(
CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")
): str,
vol.Optional(
CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "")
): str,
vol.Optional(
CONF_PATH, default=user_input.get(CONF_PATH, DEFAULT_PATH)
): str,
vol.Optional(
CONF_PATH_ZMS,
default=user_input.get(CONF_PATH_ZMS, DEFAULT_PATH_ZMS),
): str,
vol.Optional(
CONF_SSL, default=user_input.get(CONF_SSL, DEFAULT_SSL)
): bool,
vol.Optional(
CONF_VERIFY_SSL,
default=user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
): bool,
}
),
errors=errors,
)
async def async_step_finish(self, config: dict):
"""Finish config flow."""
zm_client = create_client_from_config(config)
hostname = urlparse(zm_client.get_zms_url()).hostname
result = await async_test_client_availability(self.hass, zm_client)
if result != ClientAvailabilityResult.AVAILABLE:
return self.async_abort(reason=str(result.value))
await self.async_set_unique_id(hostname)
self._abort_if_unique_id_configured(config)
return self.async_create_entry(title=hostname, data=config)

View File

@@ -1,14 +0,0 @@
"""Constants for zoneminder component."""
CONF_PATH_ZMS = "path_zms"
DEFAULT_PATH = "/zm/"
DEFAULT_PATH_ZMS = "/zm/cgi-bin/nph-zms"
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True
DOMAIN = "zoneminder"
SERVICE_SET_RUN_STATE = "set_run_state"
PLATFORM_CONFIGS = "platform_configs"
CONFIG_DATA = "config_data"
API_CLIENT = "api_client"

View File

@@ -1,8 +1,7 @@
{
"domain": "zoneminder",
"name": "ZoneMinder",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zoneminder",
"requirements": ["zm-py==0.4.0"],
"codeowners": ["@rohankapoorcom", "@vangorra"]
"codeowners": ["@rohankapoorcom"]
}

View File

@@ -1,19 +1,15 @@
"""Support for ZoneMinder sensors."""
import logging
from typing import Callable, List, Optional
import voluptuous as vol
from zoneminder.monitor import Monitor, TimePeriod
from zoneminder.zm import ZoneMinder
from zoneminder.monitor import TimePeriod
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA
from homeassistant.config_entries import ConfigEntry
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from .common import get_client_from_data, get_platform_configs
from . import DOMAIN as ZONEMINDER_DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -41,50 +37,35 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: Callable[[List[Entity], Optional[bool]], None],
) -> None:
"""Set up the sensor config entry."""
zm_client = get_client_from_data(hass, config_entry.unique_id)
monitors = await hass.async_add_job(zm_client.get_monitors)
if not monitors:
_LOGGER.warning("Did not fetch any monitors from ZoneMinder")
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder sensor platform."""
include_archived = config.get(CONF_INCLUDE_ARCHIVED)
sensors = []
for monitor in monitors:
sensors.append(ZMSensorMonitors(monitor, config_entry))
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
monitors = zm_client.get_monitors()
if not monitors:
_LOGGER.warning("Could not fetch any monitors from ZoneMinder")
for config in get_platform_configs(hass, SENSOR_DOMAIN):
include_archived = config.get(CONF_INCLUDE_ARCHIVED)
for monitor in monitors:
sensors.append(ZMSensorMonitors(monitor))
for sensor in config[CONF_MONITORED_CONDITIONS]:
sensors.append(
ZMSensorEvents(monitor, include_archived, sensor, config_entry)
)
sensors.append(ZMSensorEvents(monitor, include_archived, sensor))
sensors.append(ZMSensorRunState(zm_client, config_entry))
async_add_entities(sensors, True)
sensors.append(ZMSensorRunState(zm_client))
add_entities(sensors)
class ZMSensorMonitors(Entity):
"""Get the status of each ZoneMinder monitor."""
def __init__(self, monitor: Monitor, config_entry: ConfigEntry):
def __init__(self, monitor):
"""Initialize monitor sensor."""
self._monitor = monitor
self._config_entry = config_entry
self._state = None
self._is_available = None
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return f"{self._config_entry.unique_id}_{self._monitor.id}_status"
@property
def name(self):
"""Return the name of the sensor."""
@@ -113,26 +94,14 @@ class ZMSensorMonitors(Entity):
class ZMSensorEvents(Entity):
"""Get the number of events for each monitor."""
def __init__(
self,
monitor: Monitor,
include_archived: bool,
sensor_type: str,
config_entry: ConfigEntry,
):
def __init__(self, monitor, include_archived, sensor_type):
"""Initialize event sensor."""
self._monitor = monitor
self._include_archived = include_archived
self.time_period = TimePeriod.get_time_period(sensor_type)
self._config_entry = config_entry
self._state = None
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return f"{self._config_entry.unique_id}_{self._monitor.id}_{self.time_period.value}_{self._include_archived}_events"
@property
def name(self):
"""Return the name of the sensor."""
@@ -156,17 +125,11 @@ class ZMSensorEvents(Entity):
class ZMSensorRunState(Entity):
"""Get the ZoneMinder run state."""
def __init__(self, client: ZoneMinder, config_entry: ConfigEntry):
def __init__(self, client):
"""Initialize run state sensor."""
self._state = None
self._is_available = None
self._client = client
self._config_entry = config_entry
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return f"{self._config_entry.unique_id}_runstate"
@property
def name(self):

View File

@@ -1,9 +1,6 @@
set_run_state:
description: "Set the ZoneMinder run state"
description: Set the ZoneMinder run state
fields:
id:
description: "The host name or IP address of the ZoneMinder instance."
example: "10.10.0.2"
name:
description: "The string name of the ZoneMinder run state to set as active."
description: The string name of the ZoneMinder run state to set as active.
example: "Home"

View File

@@ -1,28 +0,0 @@
{
"config": {
"flow_title": "ZoneMinder",
"step": {
"user": {
"title": "Add ZoneMinder Server.",
"data": {
"host": "Host and Port (ex 10.10.0.4:8010)",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"path": "ZM Path",
"path_zms": "ZMS Path",
"ssl": "Use SSL for connections to ZoneMinder",
"verify_ssl": "Verify SSL Certificate"
}
}
},
"abort": {
"auth_fail": "Username or password is incorrect.",
"connection_error": "Failed to connect to a ZoneMinder server."
},
"error": {
"auth_fail": "Username or password is incorrect.",
"connection_error": "Failed to connect to a ZoneMinder server."
},
"create_entry": { "default": "ZoneMinder server added." }
}
}

View File

@@ -1,61 +1,41 @@
"""Support for ZoneMinder switches."""
import logging
from typing import Callable, List, Optional
import voluptuous as vol
from zoneminder.monitor import Monitor, MonitorState
from zoneminder.monitor import MonitorState
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
PLATFORM_SCHEMA,
SwitchEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity
from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from .common import get_client_from_data, get_platform_configs
from . import DOMAIN as ZONEMINDER_DOMAIN
_LOGGER = logging.getLogger(__name__)
MONITOR_STATES = {
MonitorState[name].value: MonitorState[name]
for name in dir(MonitorState)
if not name.startswith("_")
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_COMMAND_ON): vol.All(vol.In(MONITOR_STATES.keys())),
vol.Required(CONF_COMMAND_OFF): vol.All(vol.In(MONITOR_STATES.keys())),
vol.Required(CONF_COMMAND_ON): cv.string,
vol.Required(CONF_COMMAND_OFF): cv.string,
}
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: Callable[[List[Entity], Optional[bool]], None],
) -> None:
"""Set up the sensor config entry."""
zm_client = get_client_from_data(hass, config_entry.unique_id)
monitors = await hass.async_add_job(zm_client.get_monitors)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder switch platform."""
if not monitors:
_LOGGER.warning("Could not fetch monitors from ZoneMinder")
return
on_state = MonitorState(config.get(CONF_COMMAND_ON))
off_state = MonitorState(config.get(CONF_COMMAND_OFF))
switches = []
for monitor in monitors:
for config in get_platform_configs(hass, SWITCH_DOMAIN):
on_state = MONITOR_STATES[config[CONF_COMMAND_ON]]
off_state = MONITOR_STATES[config[CONF_COMMAND_OFF]]
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
monitors = zm_client.get_monitors()
if not monitors:
_LOGGER.warning("Could not fetch monitors from ZoneMinder")
return
switches.append(
ZMSwitchMonitors(monitor, on_state, off_state, config_entry)
)
async_add_entities(switches, True)
for monitor in monitors:
switches.append(ZMSwitchMonitors(monitor, on_state, off_state))
add_entities(switches)
class ZMSwitchMonitors(SwitchEntity):
@@ -63,25 +43,13 @@ class ZMSwitchMonitors(SwitchEntity):
icon = "mdi:record-rec"
def __init__(
self,
monitor: Monitor,
on_state: MonitorState,
off_state: MonitorState,
config_entry: ConfigEntry,
):
def __init__(self, monitor, on_state, off_state):
"""Initialize the switch."""
self._monitor = monitor
self._on_state = on_state
self._off_state = off_state
self._config_entry = config_entry
self._state = None
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return f"{self._config_entry.unique_id}_{self._monitor.id}_switch_{self._on_state.value}_{self._off_state.value}"
@property
def name(self):
"""Return the name of the switch."""

View File

@@ -488,7 +488,6 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non
CONF_UNIT_SYSTEM,
CONF_EXTERNAL_URL,
CONF_INTERNAL_URL,
CONF_MEDIA_DIRS,
]
):
hac.config_source = SOURCE_YAML

View File

@@ -1,13 +1,13 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 116
PATCH_VERSION = "0b4"
PATCH_VERSION = "3"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 1)
# Truthy date string triggers showing related deprecation warning messages.
REQUIRED_NEXT_PYTHON_VER = (3, 8, 0)
REQUIRED_NEXT_PYTHON_DATE = ""
REQUIRED_NEXT_PYTHON_DATE = "December 7, 2020"
# Format for platform files
PLATFORM_FORMAT = "{platform}.{domain}"

View File

@@ -1,8 +1,6 @@
"""The exceptions used by Home Assistant."""
from typing import TYPE_CHECKING, Optional
import jinja2
if TYPE_CHECKING:
from .core import Context # noqa: F401 pylint: disable=unused-import
@@ -22,7 +20,7 @@ class NoEntitySpecifiedError(HomeAssistantError):
class TemplateError(HomeAssistantError):
"""Error during template rendering."""
def __init__(self, exception: jinja2.TemplateError) -> None:
def __init__(self, exception: Exception) -> None:
"""Init the error."""
super().__init__(f"{exception.__class__.__name__}: {exception}")

View File

@@ -217,6 +217,5 @@ FLOWS = [
"yeelight",
"zerproc",
"zha",
"zoneminder",
"zwave"
]

View File

@@ -233,6 +233,9 @@ def async_track_state_change_event(
care about the state change events so we can
do a fast dict lookup to route events.
"""
entity_ids = _async_string_to_lower_list(entity_ids)
if not entity_ids:
return _remove_empty_listener
entity_callbacks = hass.data.setdefault(TRACK_STATE_CHANGE_CALLBACKS, {})
@@ -277,6 +280,11 @@ def async_track_state_change_event(
return remove_listener
@callback
def _remove_empty_listener() -> None:
"""Remove a listener that does nothing."""
@callback
def _async_remove_indexed_listeners(
hass: HomeAssistant,
@@ -309,6 +317,9 @@ def async_track_entity_registry_updated_event(
Similar to async_track_state_change_event.
"""
entity_ids = _async_string_to_lower_list(entity_ids)
if not entity_ids:
return _remove_empty_listener
entity_callbacks = hass.data.setdefault(TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS, {})
@@ -381,6 +392,9 @@ def async_track_state_added_domain(
action: Callable[[Event], Any],
) -> Callable[[], None]:
"""Track state change events when an entity is added to domains."""
domains = _async_string_to_lower_list(domains)
if not domains:
return _remove_empty_listener
domain_callbacks = hass.data.setdefault(TRACK_STATE_ADDED_DOMAIN_CALLBACKS, {})
@@ -424,6 +438,9 @@ def async_track_state_removed_domain(
action: Callable[[Event], Any],
) -> Callable[[], None]:
"""Track state change events when an entity is removed from domains."""
domains = _async_string_to_lower_list(domains)
if not domains:
return _remove_empty_listener
domain_callbacks = hass.data.setdefault(TRACK_STATE_REMOVED_DOMAIN_CALLBACKS, {})

View File

@@ -330,7 +330,7 @@ class Template:
try:
return compiled.render(kwargs).strip()
except jinja2.TemplateError as err:
except Exception as err: # pylint: disable=broad-except
raise TemplateError(err) from err
async def async_render_will_timeout(
@@ -587,17 +587,18 @@ class DomainStates:
class TemplateState(State):
"""Class to represent a state object in a template."""
__slots__ = ("_hass", "_state")
__slots__ = ("_hass", "_state", "_collect")
# Inheritance is done so functions that check against State keep working
# pylint: disable=super-init-not-called
def __init__(self, hass, state):
def __init__(self, hass, state, collect=True):
"""Initialize template state."""
self._hass = hass
self._state = state
self._collect = collect
def _collect_state(self):
if _RENDER_INFO in self._hass.data:
if self._collect and _RENDER_INFO in self._hass.data:
self._hass.data[_RENDER_INFO].entities.add(self._state.entity_id)
# Jinja will try __getitem__ first and it avoids the need
@@ -606,7 +607,7 @@ class TemplateState(State):
"""Return a property as an attribute for jinja."""
if item in _COLLECTABLE_STATE_ATTRIBUTES:
# _collect_state inlined here for performance
if _RENDER_INFO in self._hass.data:
if self._collect and _RENDER_INFO in self._hass.data:
self._hass.data[_RENDER_INFO].entities.add(self._state.entity_id)
return getattr(self._state, item)
if item == "entity_id":
@@ -697,7 +698,7 @@ def _collect_state(hass: HomeAssistantType, entity_id: str) -> None:
def _state_generator(hass: HomeAssistantType, domain: Optional[str]) -> Generator:
"""State generator for a domain or all states."""
for state in sorted(hass.states.async_all(domain), key=attrgetter("entity_id")):
yield TemplateState(hass, state)
yield TemplateState(hass, state, collect=False)
def _get_state_if_valid(

View File

@@ -13,11 +13,11 @@ defusedxml==0.6.0
distro==1.5.0
emoji==0.5.4
hass-nabucasa==0.37.0
home-assistant-frontend==20201001.1
home-assistant-frontend==20201001.2
importlib-metadata==1.6.0;python_version<'3.8'
jinja2>=2.11.2
netdisco==2.8.2
paho-mqtt==1.5.1
paho-mqtt==1.5.0
pillow==7.2.0
pip>=8.0.3
python-slugify==4.0.1

34
machine/intel-nuc Normal file
View File

@@ -0,0 +1,34 @@
ARG BUILD_VERSION
FROM homeassistant/amd64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
libva-intel-driver \
usbutils
##
# Build libcec for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
p8-platform-dev \
linux-headers \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& cd /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
-DHAVE_LINUX_API=1 \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec*

34
machine/odroid-c2 Normal file
View File

@@ -0,0 +1,34 @@
ARG BUILD_VERSION
FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
##
# Build libcec for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
p8-platform-dev \
linux-headers \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& cd /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
-DHAVE_LINUX_API=1 \
-DHAVE_AOCEC_API=1 \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec*

34
machine/odroid-n2 Normal file
View File

@@ -0,0 +1,34 @@
ARG BUILD_VERSION
FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
##
# Build libcec for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
p8-platform-dev \
linux-headers \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& cd /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
-DHAVE_LINUX_API=1 \
-DHAVE_AOCEC_API=1 \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec*

34
machine/odroid-xu Normal file
View File

@@ -0,0 +1,34 @@
ARG BUILD_VERSION
FROM homeassistant/armv7-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
##
# Build libcec for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
p8-platform-dev \
linux-headers \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& cd /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
-DHAVE_LINUX_API=1 \
-DHAVE_EXYNOS_API=1 \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec*

33
machine/qemuarm Normal file
View File

@@ -0,0 +1,33 @@
ARG BUILD_VERSION
FROM homeassistant/armhf-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
##
# Build libcec for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
p8-platform-dev \
linux-headers \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& cd /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
-DHAVE_LINUX_API=1 \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec*

33
machine/qemuarm-64 Normal file
View File

@@ -0,0 +1,33 @@
ARG BUILD_VERSION
FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
##
# Build libcec for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
p8-platform-dev \
linux-headers \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& cd /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
-DHAVE_LINUX_API=1 \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec*

33
machine/qemux86 Normal file
View File

@@ -0,0 +1,33 @@
ARG BUILD_VERSION
FROM homeassistant/i386-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
##
# Build libcec for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
p8-platform-dev \
linux-headers \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& cd /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
-DHAVE_LINUX_API=1 \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec*

33
machine/qemux86-64 Normal file
View File

@@ -0,0 +1,33 @@
ARG BUILD_VERSION
FROM homeassistant/amd64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
usbutils
##
# Build libcec for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
p8-platform-dev \
linux-headers \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& cd /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
-DHAVE_LINUX_API=1 \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec*

61
machine/raspberrypi Normal file
View File

@@ -0,0 +1,61 @@
ARG BUILD_VERSION
FROM homeassistant/armhf-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
raspberrypi \
raspberrypi-libs \
usbutils \
&& sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
RPi.GPIO -c /usr/src/homeassistant/requirements_all.txt
##
# Set symlinks for raspberry pi camera binaries.
RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \
&& ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \
&& ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \
&& ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv
##
# Build libcec with RPi support for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
raspberrypi-dev \
p8-platform-dev \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DRPI_INCLUDE_DIR=/opt/vc/include \
-DRPI_LIB_DIR=/opt/vc/lib \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec
ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH}
##
# Install DHT
RUN apk add --no-cache --virtual .build-dependencies \
gcc libc-dev raspberrypi-dev \
&& export DHT_VERSION="$(cat /usr/src/homeassistant/requirements_all.txt | sed -n 's|.*Adafruit-DHT==\([0-9\.]*\).*|\1|p')" \
&& git clone --depth 1 -b ${DHT_VERSION} https://github.com/adafruit/Adafruit_Python_DHT /usr/src/dht \
&& cd /usr/src/dht \
&& sed -i 's/^pi_version\ =\ None/pi_version\ =\ 1/' setup.py \
&& sed -i 's/^platform\ =\ platform_detect.UNKNOWN/platform\ =\ platform_detect.RASPBERRY_PI/' setup.py \
&& sed -i 's/platform\ =\ platform_detect.platform_detect()/pass/' setup.py \
&& export MAKEFLAGS="-j$(nproc)" \
&& pip3 install . \
&& apk del .build-dependencies \
&& rm -rf /usr/src/dht

61
machine/raspberrypi2 Normal file
View File

@@ -0,0 +1,61 @@
ARG BUILD_VERSION
FROM homeassistant/armv7-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
raspberrypi \
raspberrypi-libs \
usbutils \
&& sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
RPi.GPIO -c /usr/src/homeassistant/requirements_all.txt
##
# Set symlinks for raspberry pi binaries.
RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \
&& ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \
&& ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \
&& ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv
##
# Build libcec with RPi support for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
raspberrypi-dev \
p8-platform-dev \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DRPI_INCLUDE_DIR=/opt/vc/include \
-DRPI_LIB_DIR=/opt/vc/lib \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec
ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH}
##
# Install DHT
RUN apk add --no-cache --virtual .build-dependencies \
gcc libc-dev raspberrypi-dev \
&& export DHT_VERSION="$(cat /usr/src/homeassistant/requirements_all.txt | sed -n 's|.*Adafruit-DHT==\([0-9\.]*\).*|\1|p')" \
&& git clone --depth 1 -b ${DHT_VERSION} https://github.com/adafruit/Adafruit_Python_DHT /usr/src/dht \
&& cd /usr/src/dht \
&& sed -i 's/^pi_version\ =\ None/pi_version\ =\ 2/' setup.py \
&& sed -i 's/^platform\ =\ platform_detect.UNKNOWN/platform\ =\ platform_detect.RASPBERRY_PI/' setup.py \
&& sed -i 's/platform\ =\ platform_detect.platform_detect()/pass/' setup.py \
&& export MAKEFLAGS="-j$(nproc)" \
&& pip3 install . \
&& apk del .build-dependencies \
&& rm -rf /usr/src/dht

61
machine/raspberrypi3 Normal file
View File

@@ -0,0 +1,61 @@
ARG BUILD_VERSION
FROM homeassistant/armv7-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
raspberrypi \
raspberrypi-libs \
usbutils \
&& sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt
##
# Set symlinks for raspberry pi binaries.
RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \
&& ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \
&& ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \
&& ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv
##
# Build libcec with RPi support for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
raspberrypi-dev \
p8-platform-dev \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DRPI_INCLUDE_DIR=/opt/vc/include \
-DRPI_LIB_DIR=/opt/vc/lib \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec
ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH}
##
# Install DHT
RUN apk add --no-cache --virtual .build-dependencies \
gcc libc-dev raspberrypi-dev \
&& export DHT_VERSION="$(cat /usr/src/homeassistant/requirements_all.txt | sed -n 's|.*Adafruit-DHT==\([0-9\.]*\).*|\1|p')" \
&& git clone --depth 1 -b ${DHT_VERSION} https://github.com/adafruit/Adafruit_Python_DHT /usr/src/dht \
&& cd /usr/src/dht \
&& sed -i 's/^pi_version\ =\ None/pi_version\ =\ 3/' setup.py \
&& sed -i 's/^platform\ =\ platform_detect.UNKNOWN/platform\ =\ platform_detect.RASPBERRY_PI/' setup.py \
&& sed -i 's/platform\ =\ platform_detect.platform_detect()/pass/' setup.py \
&& export MAKEFLAGS="-j$(nproc)" \
&& pip3 install . \
&& apk del .build-dependencies \
&& rm -rf /usr/src/dht

61
machine/raspberrypi3-64 Normal file
View File

@@ -0,0 +1,61 @@
ARG BUILD_VERSION
FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
raspberrypi \
raspberrypi-libs \
usbutils \
&& sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt
##
# Set symlinks for raspberry pi binaries.
RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \
&& ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \
&& ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \
&& ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv
##
# Build libcec with RPi support for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
raspberrypi-dev \
p8-platform-dev \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DRPI_INCLUDE_DIR=/opt/vc/include \
-DRPI_LIB_DIR=/opt/vc/lib \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec
ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH}
##
# Install DHT
RUN apk add --no-cache --virtual .build-dependencies \
gcc libc-dev raspberrypi-dev \
&& export DHT_VERSION="$(cat /usr/src/homeassistant/requirements_all.txt | sed -n 's|.*Adafruit-DHT==\([0-9\.]*\).*|\1|p')" \
&& git clone --depth 1 -b ${DHT_VERSION} https://github.com/adafruit/Adafruit_Python_DHT /usr/src/dht \
&& cd /usr/src/dht \
&& sed -i 's/^pi_version\ =\ None/pi_version\ =\ 3/' setup.py \
&& sed -i 's/^platform\ =\ platform_detect.UNKNOWN/platform\ =\ platform_detect.RASPBERRY_PI/' setup.py \
&& sed -i 's/platform\ =\ platform_detect.platform_detect()/pass/' setup.py \
&& export MAKEFLAGS="-j$(nproc)" \
&& pip3 install . \
&& apk del .build-dependencies \
&& rm -rf /usr/src/dht

61
machine/raspberrypi4 Normal file
View File

@@ -0,0 +1,61 @@
ARG BUILD_VERSION
FROM homeassistant/armv7-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
raspberrypi \
raspberrypi-libs \
usbutils \
&& sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt
##
# Set symlinks for raspberry pi binaries.
RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \
&& ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \
&& ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \
&& ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv
##
# Build libcec with RPi support for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
raspberrypi-dev \
p8-platform-dev \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DRPI_INCLUDE_DIR=/opt/vc/include \
-DRPI_LIB_DIR=/opt/vc/lib \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec
ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH}
##
# Install DHT
RUN apk add --no-cache --virtual .build-dependencies \
gcc libc-dev raspberrypi-dev \
&& export DHT_VERSION="$(cat /usr/src/homeassistant/requirements_all.txt | sed -n 's|.*Adafruit-DHT==\([0-9\.]*\).*|\1|p')" \
&& git clone --depth 1 -b ${DHT_VERSION} https://github.com/adafruit/Adafruit_Python_DHT /usr/src/dht \
&& cd /usr/src/dht \
&& sed -i 's/^pi_version\ =\ None/pi_version\ =\ 3/' setup.py \
&& sed -i 's/^platform\ =\ platform_detect.UNKNOWN/platform\ =\ platform_detect.RASPBERRY_PI/' setup.py \
&& sed -i 's/platform\ =\ platform_detect.platform_detect()/pass/' setup.py \
&& export MAKEFLAGS="-j$(nproc)" \
&& pip3 install . \
&& apk del .build-dependencies \
&& rm -rf /usr/src/dht

61
machine/raspberrypi4-64 Normal file
View File

@@ -0,0 +1,61 @@
ARG BUILD_VERSION
FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION
RUN apk --no-cache add \
raspberrypi \
raspberrypi-libs \
usbutils \
&& sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt
##
# Set symlinks for raspberry pi binaries.
RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \
&& ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \
&& ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \
&& ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv
##
# Build libcec with RPi support for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
raspberrypi-dev \
p8-platform-dev \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DRPI_INCLUDE_DIR=/opt/vc/include \
-DRPI_LIB_DIR=/opt/vc/lib \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec
ENV LD_LIBRARY_PATH=/opt/vc/lib:${LD_LIBRARY_PATH}
##
# Install DHT
RUN apk add --no-cache --virtual .build-dependencies \
gcc libc-dev raspberrypi-dev \
&& export DHT_VERSION="$(cat /usr/src/homeassistant/requirements_all.txt | sed -n 's|.*Adafruit-DHT==\([0-9\.]*\).*|\1|p')" \
&& git clone --depth 1 -b ${DHT_VERSION} https://github.com/adafruit/Adafruit_Python_DHT /usr/src/dht \
&& cd /usr/src/dht \
&& sed -i 's/^pi_version\ =\ None/pi_version\ =\ 3/' setup.py \
&& sed -i 's/^platform\ =\ platform_detect.UNKNOWN/platform\ =\ platform_detect.RASPBERRY_PI/' setup.py \
&& sed -i 's/platform\ =\ platform_detect.platform_detect()/pass/' setup.py \
&& export MAKEFLAGS="-j$(nproc)" \
&& pip3 install . \
&& apk del .build-dependencies \
&& rm -rf /usr/src/dht

48
machine/tinker Normal file
View File

@@ -0,0 +1,48 @@
ARG BUILD_VERSION
FROM homeassistant/armv7-homeassistant:$BUILD_VERSION
RUN apk --no-cache add usbutils \
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
bluepy \
pybluez \
pygatt[GATTTOOL]
# Install GPIO support
RUN apk add --no-cache --virtual .build-dependencies \
gcc libc-dev musl-dev \
&& git clone --depth 1 https://github.com/TinkerBoard/gpio_lib_python /usr/src/gpio \
&& cd /usr/src/gpio \
&& sed -i "s/caddr_t/void*/g" source/wiringTB.c \
&& export MAKEFLAGS="-j$(nproc)" \
&& python3 setup.py install \
&& apk del .build-dependencies \
&& rm -rf /usr/src/gpio
##
# Build libcec for HDMI-CEC
ARG LIBCEC_VERSION=6.0.2
RUN apk add --no-cache \
eudev-libs \
p8-platform \
&& apk add --no-cache --virtual .build-dependencies \
build-base \
cmake \
eudev-dev \
swig \
p8-platform-dev \
linux-headers \
&& git clone --depth 1 -b libcec-${LIBCEC_VERSION} https://github.com/Pulse-Eight/libcec /usr/src/libcec \
&& cd /usr/src/libcec \
&& mkdir -p /usr/src/libcec/build \
&& cd /usr/src/libcec/build \
&& cmake -DCMAKE_INSTALL_PREFIX:PATH=/usr/local \
-DPYTHON_LIBRARY="/usr/local/lib/libpython3.8.so" \
-DPYTHON_INCLUDE_DIR="/usr/local/include/python3.8" \
-DHAVE_LINUX_API=1 \
.. \
&& make -j$(nproc) \
&& make install \
&& echo "cec" > "/usr/local/lib/python3.8/site-packages/cec.pth" \
&& apk del .build-dependencies \
&& rm -rf /usr/src/libcec*

View File

@@ -194,7 +194,7 @@ aioimaplib==0.7.15
aiokafka==0.6.0
# homeassistant.components.kef
aiokef==0.2.13
aiokef==0.2.16
# homeassistant.components.lifx
aiolifx==0.6.7
@@ -275,7 +275,7 @@ aqualogic==1.0
arcam-fmj==0.5.3
# homeassistant.components.arris_tg2492lg
arris-tg2492lg==1.0.0
arris-tg2492lg==1.1.0
# homeassistant.components.ampio
asmog==0.0.6
@@ -538,7 +538,7 @@ elgato==0.2.0
eliqonline==1.2.2
# homeassistant.components.elkm1
elkm1-lib==0.7.19
elkm1-lib==0.8.0
# homeassistant.components.mobile_app
emoji==0.5.4
@@ -753,7 +753,7 @@ hole==0.5.1
holidays==0.10.3
# homeassistant.components.frontend
home-assistant-frontend==20201001.1
home-assistant-frontend==20201001.2
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -1058,6 +1058,8 @@ orvibo==1.1.1
ovoenergy==1.1.7
# homeassistant.components.mqtt
paho-mqtt==1.5.0
# homeassistant.components.shiftr
paho-mqtt==1.5.1
@@ -1250,7 +1252,7 @@ pyarlo==0.2.3
pyatag==0.3.4.4
# homeassistant.components.netatmo
pyatmo==4.0.0
pyatmo==4.1.0
# homeassistant.components.atome
pyatome==0.1.1
@@ -1401,7 +1403,7 @@ pyheos==0.6.0
pyhik==0.2.7
# homeassistant.components.hive
pyhiveapi==0.2.20.1
pyhiveapi==0.2.20.2
# homeassistant.components.homematic
pyhomematic==0.1.68
@@ -1653,7 +1655,7 @@ pysmappee==0.2.13
pysmartapp==0.3.2
# homeassistant.components.smartthings
pysmartthings==0.7.3
pysmartthings==0.7.4
# homeassistant.components.smarty
pysmarty==0.8
@@ -2293,7 +2295,7 @@ xs1-api-client==3.0.0
yalesmartalarmclient==0.1.6
# homeassistant.components.yeelight
yeelight==0.5.3
yeelight==0.5.4
# homeassistant.components.yeelightsunflower
yeelightsunflower==0.0.10
@@ -2332,7 +2334,7 @@ zigpy-zigate==0.6.2
zigpy-znp==0.2.1
# homeassistant.components.zha
zigpy==0.25.0
zigpy==0.26.0
# homeassistant.components.zoneminder
zm-py==0.4.0

View File

@@ -278,7 +278,7 @@ eebrightbox==0.0.4
elgato==0.2.0
# homeassistant.components.elkm1
elkm1-lib==0.7.19
elkm1-lib==0.8.0
# homeassistant.components.mobile_app
emoji==0.5.4
@@ -376,7 +376,7 @@ hole==0.5.1
holidays==0.10.3
# homeassistant.components.frontend
home-assistant-frontend==20201001.1
home-assistant-frontend==20201001.2
# homeassistant.components.zwave
homeassistant-pyozw==0.1.10
@@ -499,8 +499,7 @@ openerz-api==0.1.0
ovoenergy==1.1.7
# homeassistant.components.mqtt
# homeassistant.components.shiftr
paho-mqtt==1.5.1
paho-mqtt==1.5.0
# homeassistant.components.panasonic_viera
panasonic_viera==0.3.6
@@ -616,7 +615,7 @@ pyarlo==0.2.3
pyatag==0.3.4.4
# homeassistant.components.netatmo
pyatmo==4.0.0
pyatmo==4.1.0
# homeassistant.components.blackbird
pyblackbird==0.5
@@ -800,7 +799,7 @@ pysmappee==0.2.13
pysmartapp==0.3.2
# homeassistant.components.smartthings
pysmartthings==0.7.3
pysmartthings==0.7.4
# homeassistant.components.soma
pysoma==0.0.10
@@ -1065,7 +1064,7 @@ wolf_smartset==0.1.6
xmltodict==0.12.0
# homeassistant.components.yeelight
yeelight==0.5.3
yeelight==0.5.4
# homeassistant.components.zeroconf
zeroconf==0.28.5
@@ -1089,7 +1088,4 @@ zigpy-zigate==0.6.2
zigpy-znp==0.2.1
# homeassistant.components.zha
zigpy==0.25.0
# homeassistant.components.zoneminder
zm-py==0.4.0
zigpy==0.26.0

View File

@@ -1213,3 +1213,66 @@ async def test_group_that_references_two_types_of_groups(hass):
assert hass.states.get("group.covers").state == "closed"
assert hass.states.get("group.device_trackers").state == "home"
assert hass.states.get("group.grouped_group").state == "on"
async def test_plant_group(hass):
"""Test plant states can be grouped."""
entity_ids = [
"plant.upstairs",
"plant.downstairs",
]
assert await async_setup_component(
hass,
"plant",
{
"plant": {
"plantname": {
"sensors": {
"moisture": "sensor.mqtt_plant_moisture",
"battery": "sensor.mqtt_plant_battery",
"temperature": "sensor.mqtt_plant_temperature",
"conductivity": "sensor.mqtt_plant_conductivity",
"brightness": "sensor.mqtt_plant_brightness",
},
"min_moisture": 20,
"max_moisture": 60,
"min_battery": 17,
"min_conductivity": 500,
"min_temperature": 15,
"min_brightness": 500,
}
}
},
)
assert await async_setup_component(
hass,
"group",
{
"group": {
"plants": {"entities": entity_ids},
"plant_with_binary_sensors": {
"entities": [*entity_ids, "binary_sensor.planter"]
},
}
},
)
await hass.async_block_till_done()
hass.states.async_set("binary_sensor.planter", "off")
for entity_id in entity_ids:
hass.states.async_set(entity_id, "ok")
await hass.async_block_till_done()
await hass.async_block_till_done()
assert hass.states.get("group.plants").state == "ok"
assert hass.states.get("group.plant_with_binary_sensors").state == "off"
hass.states.async_set("binary_sensor.planter", "on")
for entity_id in entity_ids:
hass.states.async_set(entity_id, "problem")
await hass.async_block_till_done()
assert hass.states.get("group.plants").state == "problem"
assert hass.states.get("group.plant_with_binary_sensors").state == "on"

View File

@@ -4,6 +4,7 @@ from datetime import datetime, timedelta
import unittest
import pytest
from sqlalchemy.exc import OperationalError
from homeassistant.components.recorder import (
CONFIG_SCHEMA,
@@ -452,3 +453,41 @@ def test_run_information(hass_recorder):
class CannotSerializeMe:
"""A class that the JSONEncoder cannot serialize."""
def test_saving_state_with_exception(hass, hass_recorder, caplog):
"""Test saving and restoring a state."""
hass = hass_recorder()
entity_id = "test.recorder"
state = "restoring_from_db"
attributes = {"test_attr": 5, "test_attr_10": "nice"}
def _throw_if_state_in_session(*args, **kwargs):
for obj in hass.data[DATA_INSTANCE].event_session:
if isinstance(obj, States):
raise OperationalError(
"insert the state", "fake params", "forced to fail"
)
with patch("time.sleep"), patch.object(
hass.data[DATA_INSTANCE].event_session,
"flush",
side_effect=_throw_if_state_in_session,
):
hass.states.set(entity_id, "fail", attributes)
wait_recording_done(hass)
assert "Error executing query" in caplog.text
assert "Error saving events" not in caplog.text
caplog.clear()
hass.states.set(entity_id, state, attributes)
wait_recording_done(hass)
with session_scope(hass=hass) as session:
db_states = list(session.query(States))
assert len(db_states) >= 1
assert "Error executing query" not in caplog.text
assert "Error saving events" not in caplog.text

View File

@@ -98,10 +98,9 @@ async def test_flow_user(hass: HomeAssistantType):
"""Test config flow: discovered + configured through user."""
udn = "uuid:device_1"
mock_device = MockDevice(udn)
usn = f"{mock_device.udn}::{mock_device.device_type}"
discovery_infos = [
{
DISCOVERY_USN: usn,
DISCOVERY_USN: mock_device.unique_id,
DISCOVERY_ST: mock_device.device_type,
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_LOCATION: "dummy",
@@ -121,7 +120,7 @@ async def test_flow_user(hass: HomeAssistantType):
# Confirmed via step user.
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={"usn": usn},
user_input={"usn": mock_device.unique_id},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
@@ -132,14 +131,13 @@ async def test_flow_user(hass: HomeAssistantType):
}
async def test_flow_config(hass: HomeAssistantType):
async def test_flow_import(hass: HomeAssistantType):
"""Test config flow: discovered + configured through configuration.yaml."""
udn = "uuid:device_1"
mock_device = MockDevice(udn)
usn = f"{mock_device.udn}::{mock_device.device_type}"
discovery_infos = [
{
DISCOVERY_USN: usn,
DISCOVERY_USN: mock_device.unique_id,
DISCOVERY_ST: mock_device.device_type,
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_LOCATION: "dummy",
@@ -162,6 +160,66 @@ async def test_flow_config(hass: HomeAssistantType):
}
async def test_flow_import_duplicate(hass: HomeAssistantType):
"""Test config flow: discovered, but already configured."""
udn = "uuid:device_1"
mock_device = MockDevice(udn)
discovery_infos = [
{
DISCOVERY_USN: mock_device.unique_id,
DISCOVERY_ST: mock_device.device_type,
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_LOCATION: "dummy",
}
]
# Existing entry.
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONFIG_ENTRY_UDN: mock_device.udn,
CONFIG_ENTRY_ST: mock_device.device_type,
},
options={CONFIG_ENTRY_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL},
)
config_entry.add_to_hass(hass)
with patch.object(
Device, "async_create_device", AsyncMock(return_value=mock_device)
), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)):
# Discovered via step import.
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"
async def test_flow_import_incomplete(hass: HomeAssistantType):
"""Test config flow: incomplete discovery, configured through configuration.yaml."""
udn = "uuid:device_1"
mock_device = MockDevice(udn)
discovery_infos = [
{
DISCOVERY_ST: mock_device.device_type,
DISCOVERY_UDN: mock_device.udn,
DISCOVERY_LOCATION: "dummy",
}
]
with patch.object(
Device, "async_create_device", AsyncMock(return_value=mock_device)
), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)):
# Discovered via step import.
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "incomplete_discovery"
async def test_options_flow(hass: HomeAssistantType):
"""Test options flow."""
# Set up config entry.

View File

@@ -488,6 +488,41 @@ async def test_render_template_with_error(hass, websocket_client, caplog):
assert "TemplateError" not in caplog.text
async def test_render_template_with_timeout_and_error(hass, websocket_client, caplog):
"""Test a template with an error with a timeout."""
await websocket_client.send_json(
{
"id": 5,
"type": "render_template",
"template": "{{ now() | rando }}",
"timeout": 5,
}
)
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert not msg["success"]
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
assert "TemplateError" not in caplog.text
async def test_render_template_error_in_template_code(hass, websocket_client, caplog):
"""Test a template that will throw in template.py."""
await websocket_client.send_json(
{"id": 5, "type": "render_template", "template": "{{ now() | random }}"}
)
msg = await websocket_client.receive_json()
assert msg["id"] == 5
assert msg["type"] == const.TYPE_RESULT
assert not msg["success"]
assert msg["error"]["code"] == const.ERR_TEMPLATE_ERROR
assert "TemplateError" not in caplog.text
async def test_render_template_with_delayed_error(hass, websocket_client, caplog):
"""Test a template with an error that only happens after a state change."""
hass.states.async_set("sensor.test", "on")

View File

@@ -1 +0,0 @@
"""Tests for the zoneminder component."""

View File

@@ -1,65 +0,0 @@
"""Binary sensor tests."""
from zoneminder.zm import ZoneMinder
from homeassistant.components.zoneminder import const
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant
from homeassistant.setup import async_setup_component
from tests.async_mock import MagicMock, patch
from tests.common import MockConfigEntry
async def test_async_setup_entry(hass: HomeAssistant) -> None:
"""Test setup of binary sensor entities."""
with patch(
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
) as zoneminder_mock:
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
zm_client.login.return_value = True
zm_client.is_available = True
zoneminder_mock.return_value = zm_client
config_entry = MockConfigEntry(
domain=const.DOMAIN,
unique_id="host1",
data={
CONF_HOST: "host1",
CONF_USERNAME: "username1",
CONF_PASSWORD: "password1",
CONF_PATH: "path1",
const.CONF_PATH_ZMS: "path_zms1",
CONF_SSL: False,
CONF_VERIFY_SSL: True,
},
)
config_entry.add_to_hass(hass)
await async_process_ha_core_config(hass, {})
await async_setup_component(hass, HASS_DOMAIN, {})
await async_setup_component(hass, const.DOMAIN, {})
await hass.async_block_till_done()
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "binary_sensor.host1"}
)
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.host1").state == "on"
zm_client.is_available = False
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "binary_sensor.host1"}
)
await hass.async_block_till_done()
assert hass.states.get("binary_sensor.host1").state == "off"

View File

@@ -1,89 +0,0 @@
"""Binary sensor tests."""
from zoneminder.monitor import Monitor
from zoneminder.zm import ZoneMinder
from homeassistant.components.zoneminder import const
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant
from homeassistant.setup import async_setup_component
from tests.async_mock import MagicMock, patch
from tests.common import MockConfigEntry
async def test_async_setup_entry(hass: HomeAssistant) -> None:
"""Test setup of camera entities."""
with patch(
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
) as zoneminder_mock:
monitor1 = MagicMock(spec=Monitor)
monitor1.name = "monitor1"
monitor1.mjpeg_image_url = "mjpeg_image_url1"
monitor1.still_image_url = "still_image_url1"
monitor1.is_recording = True
monitor1.is_available = True
monitor2 = MagicMock(spec=Monitor)
monitor2.name = "monitor2"
monitor2.mjpeg_image_url = "mjpeg_image_url2"
monitor2.still_image_url = "still_image_url2"
monitor2.is_recording = False
monitor2.is_available = False
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
zm_client.login.return_value = True
zm_client.get_monitors.return_value = [monitor1, monitor2]
zoneminder_mock.return_value = zm_client
config_entry = MockConfigEntry(
domain=const.DOMAIN,
unique_id="host1",
data={
CONF_HOST: "host1",
CONF_USERNAME: "username1",
CONF_PASSWORD: "password1",
CONF_PATH: "path1",
const.CONF_PATH_ZMS: "path_zms1",
CONF_SSL: False,
CONF_VERIFY_SSL: True,
},
)
config_entry.add_to_hass(hass)
await async_process_ha_core_config(hass, {})
await async_setup_component(hass, HASS_DOMAIN, {})
await async_setup_component(hass, const.DOMAIN, {})
await hass.async_block_till_done()
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor1"}
)
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor2"}
)
await hass.async_block_till_done()
assert hass.states.get("camera.monitor1").state == "recording"
assert hass.states.get("camera.monitor2").state == "unavailable"
monitor1.is_recording = False
monitor2.is_recording = True
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor1"}
)
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "camera.monitor2"}
)
await hass.async_block_till_done()
assert hass.states.get("camera.monitor1").state == "idle"
assert hass.states.get("camera.monitor2").state == "unavailable"

View File

@@ -1,119 +0,0 @@
"""Config flow tests."""
import requests
from zoneminder.zm import ZoneMinder
from homeassistant import config_entries
from homeassistant.components.zoneminder import ClientAvailabilityResult, const
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
CONF_SOURCE,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from tests.async_mock import MagicMock, patch
async def test_import(hass: HomeAssistant) -> None:
"""Test import from configuration yaml."""
with patch(
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
) as zoneminder_mock:
conf_data = {
CONF_HOST: "host1",
CONF_USERNAME: "username1",
CONF_PASSWORD: "password1",
CONF_PATH: "path1",
const.CONF_PATH_ZMS: "path_zms1",
CONF_SSL: False,
CONF_VERIFY_SSL: True,
}
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
zoneminder_mock.return_value = zm_client
zm_client.login.return_value = False
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=conf_data,
)
assert result
assert result["type"] == "abort"
assert result["reason"] == "auth_fail"
zm_client.login.return_value = True
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=conf_data,
)
assert result
assert result["type"] == "create_entry"
assert result["data"] == {
**conf_data,
CONF_SOURCE: config_entries.SOURCE_IMPORT,
}
async def test_user(hass: HomeAssistant) -> None:
"""Test user initiated creation."""
with patch(
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
) as zoneminder_mock:
conf_data = {
CONF_HOST: "host1",
CONF_USERNAME: "username1",
CONF_PASSWORD: "password1",
CONF_PATH: "path1",
const.CONF_PATH_ZMS: "path_zms1",
CONF_SSL: False,
CONF_VERIFY_SSL: True,
}
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result
assert result["type"] == "form"
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
zoneminder_mock.return_value = zm_client
zm_client.login.side_effect = requests.exceptions.ConnectionError()
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
conf_data,
)
assert result
assert result["type"] == "form"
assert result["errors"] == {
"base": ClientAvailabilityResult.ERROR_CONNECTION_ERROR.value
}
zm_client.login.side_effect = None
zm_client.login.return_value = False
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
conf_data,
)
assert result
assert result["type"] == "form"
assert result["errors"] == {
"base": ClientAvailabilityResult.ERROR_AUTH_FAIL.value
}
zm_client.login.return_value = True
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
conf_data,
)
assert result
assert result["type"] == "create_entry"
assert result["data"] == conf_data

View File

@@ -1,122 +0,0 @@
"""Tests for init functions."""
from datetime import timedelta
from zoneminder.zm import ZoneMinder
from homeassistant import config_entries
from homeassistant.components.zoneminder import const
from homeassistant.components.zoneminder.common import is_client_in_data
from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_RETRY,
)
from homeassistant.const import (
ATTR_ID,
ATTR_NAME,
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
CONF_SOURCE,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.async_mock import MagicMock, patch
from tests.common import async_fire_time_changed
async def test_no_yaml_config(hass: HomeAssistant) -> None:
"""Test empty yaml config."""
with patch(
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
) as zoneminder_mock:
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
zm_client.login.return_value = True
zm_client.get_monitors.return_value = []
zoneminder_mock.return_value = zm_client
hass_config = {const.DOMAIN: []}
await async_setup_component(hass, const.DOMAIN, hass_config)
await hass.async_block_till_done()
assert not hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
async def test_yaml_config_import(hass: HomeAssistant) -> None:
"""Test yaml config import."""
with patch(
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
) as zoneminder_mock:
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
zm_client.login.return_value = True
zm_client.get_monitors.return_value = []
zoneminder_mock.return_value = zm_client
hass_config = {const.DOMAIN: [{CONF_HOST: "host1"}]}
await async_setup_component(hass, const.DOMAIN, hass_config)
await hass.async_block_till_done()
assert hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
async def test_load_call_service_and_unload(hass: HomeAssistant) -> None:
"""Test config entry load/unload and calling of service."""
with patch(
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
) as zoneminder_mock:
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
zm_client.login.side_effect = [True, True, False, True]
zm_client.get_monitors.return_value = []
zm_client.is_available.return_value = True
zoneminder_mock.return_value = zm_client
await hass.config_entries.flow.async_init(
const.DOMAIN,
context={CONF_SOURCE: config_entries.SOURCE_USER},
data={
CONF_HOST: "host1",
CONF_USERNAME: "username1",
CONF_PASSWORD: "password1",
CONF_PATH: "path1",
const.CONF_PATH_ZMS: "path_zms1",
CONF_SSL: False,
CONF_VERIFY_SSL: True,
},
)
await hass.async_block_till_done()
config_entry = next(iter(hass.config_entries.async_entries(const.DOMAIN)), None)
assert config_entry
assert config_entry.state == ENTRY_STATE_SETUP_RETRY
assert not is_client_in_data(hass, "host1")
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert config_entry.state == ENTRY_STATE_LOADED
assert is_client_in_data(hass, "host1")
assert hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE)
await hass.services.async_call(
const.DOMAIN,
const.SERVICE_SET_RUN_STATE,
{ATTR_ID: "host1", ATTR_NAME: "away"},
)
await hass.async_block_till_done()
zm_client.set_active_state.assert_called_with("away")
await config_entry.async_unload(hass)
await hass.async_block_till_done()
assert config_entry.state == ENTRY_STATE_NOT_LOADED
assert not is_client_in_data(hass, "host1")
assert not hass.services.has_service(const.DOMAIN, const.SERVICE_SET_RUN_STATE)

View File

@@ -1,167 +0,0 @@
"""Binary sensor tests."""
from zoneminder.monitor import Monitor, MonitorState, TimePeriod
from zoneminder.zm import ZoneMinder
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.zoneminder import const
from homeassistant.components.zoneminder.sensor import CONF_INCLUDE_ARCHIVED
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
CONF_MONITORED_CONDITIONS,
CONF_PASSWORD,
CONF_PATH,
CONF_PLATFORM,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant
from homeassistant.setup import async_setup_component
from tests.async_mock import MagicMock, patch
from tests.common import MockConfigEntry
async def test_async_setup_entry(hass: HomeAssistant) -> None:
"""Test setup of sensor entities."""
def _get_events(monitor_id: int, time_period: TimePeriod, include_archived: bool):
enum_list = [name for name in dir(TimePeriod) if not name.startswith("_")]
tp_index = enum_list.index(time_period.name)
return (100 * monitor_id) + (tp_index * 10) + include_archived
def _monitor1_get_events(time_period: TimePeriod, include_archived: bool):
return _get_events(1, time_period, include_archived)
def _monitor2_get_events(time_period: TimePeriod, include_archived: bool):
return _get_events(2, time_period, include_archived)
with patch(
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
) as zoneminder_mock:
monitor1 = MagicMock(spec=Monitor)
monitor1.name = "monitor1"
monitor1.mjpeg_image_url = "mjpeg_image_url1"
monitor1.still_image_url = "still_image_url1"
monitor1.is_recording = True
monitor1.is_available = True
monitor1.function = MonitorState.MONITOR
monitor1.get_events.side_effect = _monitor1_get_events
monitor2 = MagicMock(spec=Monitor)
monitor2.name = "monitor2"
monitor2.mjpeg_image_url = "mjpeg_image_url2"
monitor2.still_image_url = "still_image_url2"
monitor2.is_recording = False
monitor2.is_available = False
monitor2.function = MonitorState.MODECT
monitor2.get_events.side_effect = _monitor2_get_events
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
zm_client.login.return_value = True
zm_client.get_monitors.return_value = [monitor1, monitor2]
zoneminder_mock.return_value = zm_client
config_entry = MockConfigEntry(
domain=const.DOMAIN,
unique_id="host1",
data={
CONF_HOST: "host1",
CONF_USERNAME: "username1",
CONF_PASSWORD: "password1",
CONF_PATH: "path1",
const.CONF_PATH_ZMS: "path_zms1",
CONF_SSL: False,
CONF_VERIFY_SSL: True,
},
)
config_entry.add_to_hass(hass)
hass_config = {
HASS_DOMAIN: {},
SENSOR_DOMAIN: [
{
CONF_PLATFORM: const.DOMAIN,
CONF_INCLUDE_ARCHIVED: True,
CONF_MONITORED_CONDITIONS: ["all", "day"],
}
],
}
await async_process_ha_core_config(hass, hass_config[HASS_DOMAIN])
await async_setup_component(hass, HASS_DOMAIN, hass_config)
await async_setup_component(hass, SENSOR_DOMAIN, hass_config)
await hass.async_block_till_done()
await async_setup_component(hass, const.DOMAIN, hass_config)
await hass.async_block_till_done()
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_status"}
)
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_events"}
)
await hass.services.async_call(
HASS_DOMAIN,
"update_entity",
{ATTR_ENTITY_ID: "sensor.monitor1_events_last_day"},
)
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_status"}
)
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_events"}
)
await hass.services.async_call(
HASS_DOMAIN,
"update_entity",
{ATTR_ENTITY_ID: "sensor.monitor2_events_last_day"},
)
await hass.async_block_till_done()
assert (
hass.states.get("sensor.monitor1_status").state
== MonitorState.MONITOR.value
)
assert hass.states.get("sensor.monitor1_events").state == "101"
assert hass.states.get("sensor.monitor1_events_last_day").state == "111"
assert hass.states.get("sensor.monitor2_status").state == "unavailable"
assert hass.states.get("sensor.monitor2_events").state == "201"
assert hass.states.get("sensor.monitor2_events_last_day").state == "211"
monitor1.function = MonitorState.NONE
monitor2.function = MonitorState.NODECT
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_status"}
)
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor1_events"}
)
await hass.services.async_call(
HASS_DOMAIN,
"update_entity",
{ATTR_ENTITY_ID: "sensor.monitor1_events_last_day"},
)
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_status"}
)
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "sensor.monitor2_events"}
)
await hass.services.async_call(
HASS_DOMAIN,
"update_entity",
{ATTR_ENTITY_ID: "sensor.monitor2_events_last_day"},
)
await hass.async_block_till_done()
assert (
hass.states.get("sensor.monitor1_status").state == MonitorState.NONE.value
)
assert hass.states.get("sensor.monitor1_events").state == "101"
assert hass.states.get("sensor.monitor1_events_last_day").state == "111"
assert hass.states.get("sensor.monitor2_status").state == "unavailable"
assert hass.states.get("sensor.monitor2_events").state == "201"
assert hass.states.get("sensor.monitor2_events_last_day").state == "211"

View File

@@ -1,126 +0,0 @@
"""Binary sensor tests."""
from zoneminder.monitor import Monitor, MonitorState
from zoneminder.zm import ZoneMinder
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.zoneminder import const
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_COMMAND_OFF,
CONF_COMMAND_ON,
CONF_HOST,
CONF_PASSWORD,
CONF_PATH,
CONF_PLATFORM,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
STATE_OFF,
STATE_ON,
)
from homeassistant.core import DOMAIN as HASS_DOMAIN, HomeAssistant
from homeassistant.setup import async_setup_component
from tests.async_mock import MagicMock, patch
from tests.common import MockConfigEntry
async def test_async_setup_entry(hass: HomeAssistant) -> None:
"""Test setup of sensor entities."""
with patch(
"homeassistant.components.zoneminder.common.ZoneMinder", autospec=ZoneMinder
) as zoneminder_mock:
monitor1 = MagicMock(spec=Monitor)
monitor1.name = "monitor1"
monitor1.mjpeg_image_url = "mjpeg_image_url1"
monitor1.still_image_url = "still_image_url1"
monitor1.is_recording = True
monitor1.is_available = True
monitor1.function = MonitorState.MONITOR
monitor2 = MagicMock(spec=Monitor)
monitor2.name = "monitor2"
monitor2.mjpeg_image_url = "mjpeg_image_url2"
monitor2.still_image_url = "still_image_url2"
monitor2.is_recording = False
monitor2.is_available = False
monitor2.function = MonitorState.MODECT
zm_client: ZoneMinder = MagicMock(spec=ZoneMinder)
zm_client.get_zms_url.return_value = "http://host1/path_zms1"
zm_client.login.return_value = True
zm_client.get_monitors.return_value = [monitor1, monitor2]
zoneminder_mock.return_value = zm_client
config_entry = MockConfigEntry(
domain=const.DOMAIN,
unique_id="host1",
data={
CONF_HOST: "host1",
CONF_USERNAME: "username1",
CONF_PASSWORD: "password1",
CONF_PATH: "path1",
const.CONF_PATH_ZMS: "path_zms1",
CONF_SSL: False,
CONF_VERIFY_SSL: True,
},
)
config_entry.add_to_hass(hass)
hass_config = {
HASS_DOMAIN: {},
SWITCH_DOMAIN: [
{
CONF_PLATFORM: const.DOMAIN,
CONF_COMMAND_ON: MonitorState.MONITOR.value,
CONF_COMMAND_OFF: MonitorState.MODECT.value,
},
{
CONF_PLATFORM: const.DOMAIN,
CONF_COMMAND_ON: MonitorState.MODECT.value,
CONF_COMMAND_OFF: MonitorState.MONITOR.value,
},
],
}
await async_process_ha_core_config(hass, hass_config[HASS_DOMAIN])
await async_setup_component(hass, HASS_DOMAIN, hass_config)
await async_setup_component(hass, SWITCH_DOMAIN, hass_config)
await hass.async_block_till_done()
await async_setup_component(hass, const.DOMAIN, hass_config)
await hass.async_block_till_done()
await hass.services.async_call(
SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: "switch.monitor1_state"}
)
await hass.services.async_call(
SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: "switch.monitor1_state_2"}
)
await hass.async_block_till_done()
assert hass.states.get("switch.monitor1_state").state == STATE_ON
assert hass.states.get("switch.monitor1_state_2").state == STATE_OFF
await hass.services.async_call(
SWITCH_DOMAIN, "turn_off", {ATTR_ENTITY_ID: "switch.monitor1_state"}
)
await hass.services.async_call(
SWITCH_DOMAIN, "turn_on", {ATTR_ENTITY_ID: "switch.monitor1_state_2"}
)
await hass.async_block_till_done()
assert hass.states.get("switch.monitor1_state").state == STATE_OFF
assert hass.states.get("switch.monitor1_state_2").state == STATE_ON
monitor1.function = MonitorState.NONE
monitor2.function = MonitorState.NODECT
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "switch.monitor1_state"}
)
await hass.services.async_call(
HASS_DOMAIN, "update_entity", {ATTR_ENTITY_ID: "switch.monitor1_state_2"}
)
await hass.async_block_till_done()
assert hass.states.get("switch.monitor1_state").state == STATE_OFF
assert hass.states.get("switch.monitor1_state_2").state == STATE_OFF

View File

@@ -486,6 +486,19 @@ async def test_async_track_state_change_event(hass):
unsub_throws()
async def test_async_track_state_change_event_with_empty_list(hass):
"""Test async_track_state_change_event passing an empty list of entities."""
unsub_single = async_track_state_change_event(
hass, [], ha.callback(lambda event: None)
)
unsub_single2 = async_track_state_change_event(
hass, [], ha.callback(lambda event: None)
)
unsub_single2()
unsub_single()
async def test_async_track_state_added_domain(hass):
"""Test async_track_state_added_domain."""
single_entity_id_tracker = []
@@ -568,6 +581,32 @@ async def test_async_track_state_added_domain(hass):
unsub_throws()
async def test_async_track_state_added_domain_with_empty_list(hass):
"""Test async_track_state_added_domain passing an empty list of domains."""
unsub_single = async_track_state_added_domain(
hass, [], ha.callback(lambda event: None)
)
unsub_single2 = async_track_state_added_domain(
hass, [], ha.callback(lambda event: None)
)
unsub_single2()
unsub_single()
async def test_async_track_state_removed_domain_with_empty_list(hass):
"""Test async_track_state_removed_domain passing an empty list of domains."""
unsub_single = async_track_state_removed_domain(
hass, [], ha.callback(lambda event: None)
)
unsub_single2 = async_track_state_removed_domain(
hass, [], ha.callback(lambda event: None)
)
unsub_single2()
unsub_single()
async def test_async_track_state_removed_domain(hass):
"""Test async_track_state_removed_domain."""
single_entity_id_tracker = []
@@ -1284,7 +1323,7 @@ async def test_track_template_result_iterator(hass):
assert info.listeners == {
"all": False,
"domains": {"sensor"},
"entities": {"sensor.test"},
"entities": set(),
}
hass.states.async_set("sensor.test", 6)
@@ -1488,6 +1527,80 @@ async def test_track_template_rate_limit_five(hass):
assert refresh_runs == ["0", "1"]
async def test_track_template_has_default_rate_limit(hass):
"""Test template has a rate limit by default."""
hass.states.async_set("sensor.zero", "any")
template_refresh = Template("{{ states | list | count }}", hass)
refresh_runs = []
@ha.callback
def refresh_listener(event, updates):
refresh_runs.append(updates.pop().result)
info = async_track_template_result(
hass,
[TrackTemplate(template_refresh, None)],
refresh_listener,
)
await hass.async_block_till_done()
info.async_refresh()
await hass.async_block_till_done()
assert refresh_runs == ["1"]
hass.states.async_set("sensor.one", "any")
await hass.async_block_till_done()
assert refresh_runs == ["1"]
info.async_refresh()
assert refresh_runs == ["1", "2"]
hass.states.async_set("sensor.two", "any")
await hass.async_block_till_done()
assert refresh_runs == ["1", "2"]
hass.states.async_set("sensor.three", "any")
await hass.async_block_till_done()
assert refresh_runs == ["1", "2"]
async def test_track_template_unavailable_sates_has_default_rate_limit(hass):
"""Test template watching for unavailable states has a rate limit by default."""
hass.states.async_set("sensor.zero", "unknown")
template_refresh = Template(
"{{ states | selectattr('state', 'in', ['unavailable', 'unknown', 'none']) | list | count }}",
hass,
)
refresh_runs = []
@ha.callback
def refresh_listener(event, updates):
refresh_runs.append(updates.pop().result)
info = async_track_template_result(
hass,
[TrackTemplate(template_refresh, None)],
refresh_listener,
)
await hass.async_block_till_done()
info.async_refresh()
await hass.async_block_till_done()
assert refresh_runs == ["1"]
hass.states.async_set("sensor.one", "unknown")
await hass.async_block_till_done()
assert refresh_runs == ["1"]
info.async_refresh()
assert refresh_runs == ["1", "2"]
hass.states.async_set("sensor.two", "any")
await hass.async_block_till_done()
assert refresh_runs == ["1", "2"]
hass.states.async_set("sensor.three", "unknown")
await hass.async_block_till_done()
assert refresh_runs == ["1", "2"]
info.async_refresh()
await hass.async_block_till_done()
assert refresh_runs == ["1", "2", "3"]
async def test_specifically_referenced_entity_is_not_rate_limited(hass):
"""Test template rate limit of 5 seconds."""
hass.states.async_set("sensor.one", "none")
@@ -2802,3 +2915,16 @@ async def test_async_track_entity_registry_updated_event_with_a_callback_that_th
unsub2()
assert event_data[0] == {"action": "create", "entity_id": "switch.puppy_feeder"}
async def test_async_track_entity_registry_updated_event_with_empty_list(hass):
"""Test async_track_entity_registry_updated_event passing an empty list of entities."""
unsub_single = hass.helpers.event.async_track_entity_registry_updated_event(
[], ha.callback(lambda event: None)
)
unsub_single2 = hass.helpers.event.async_track_entity_registry_updated_event(
[], ha.callback(lambda event: None)
)
unsub_single2()
unsub_single()

View File

@@ -155,9 +155,25 @@ def test_iterating_all_states(hass):
hass.states.async_set("sensor.temperature", 10)
info = render_to_info(hass, tmpl_str)
assert_result_info(
info, "10happy", entities=["test.object", "sensor.temperature"], all_states=True
)
assert_result_info(info, "10happy", entities=[], all_states=True)
def test_iterating_all_states_unavailable(hass):
"""Test iterating all states unavailable."""
hass.states.async_set("test.object", "on")
tmpl_str = "{{ states | selectattr('state', 'in', ['unavailable', 'unknown', 'none']) | list | count }}"
info = render_to_info(hass, tmpl_str)
assert info.all_states is True
assert info.rate_limit == template.DEFAULT_RATE_LIMIT
hass.states.async_set("test.object", "unknown")
hass.states.async_set("sensor.temperature", 10)
info = render_to_info(hass, tmpl_str)
assert_result_info(info, "1", entities=[], all_states=True)
def test_iterating_domain_states(hass):
@@ -176,7 +192,7 @@ def test_iterating_domain_states(hass):
assert_result_info(
info,
"open10",
entities=["sensor.back_door", "sensor.temperature"],
entities=[],
domains=["sensor"],
)
@@ -1426,9 +1442,7 @@ async def test_expand(hass):
info = render_to_info(
hass, "{{ expand(states.group) | map(attribute='entity_id') | join(', ') }}"
)
assert_result_info(
info, "test.object", {"test.object", "group.new_group"}, ["group"]
)
assert_result_info(info, "test.object", {"test.object"}, ["group"])
assert info.rate_limit == template.DEFAULT_RATE_LIMIT
info = render_to_info(
@@ -1587,7 +1601,7 @@ async def test_async_render_to_info_with_wildcard_matching_entity_id(hass):
"""Test tracking template with a wildcard."""
template_complex_str = r"""
{% for state in states %}
{% for state in states.cover %}
{% if state.entity_id | regex_match('.*\.office_') %}
{{ state.entity_id }}={{ state.state }}
{% endif %}
@@ -1599,13 +1613,9 @@ async def test_async_render_to_info_with_wildcard_matching_entity_id(hass):
hass.states.async_set("cover.office_skylight", "open")
info = render_to_info(hass, template_complex_str)
assert not info.domains
assert info.entities == {
"cover.office_drapes",
"cover.office_window",
"cover.office_skylight",
}
assert info.all_states is True
assert info.domains == {"cover"}
assert info.entities == set()
assert info.all_states is False
assert info.rate_limit == template.DEFAULT_RATE_LIMIT
@@ -1629,13 +1639,7 @@ async def test_async_render_to_info_with_wildcard_matching_state(hass):
info = render_to_info(hass, template_complex_str)
assert not info.domains
assert info.entities == {
"cover.x_skylight",
"binary_sensor.door",
"cover.office_drapes",
"cover.office_window",
"cover.office_skylight",
}
assert info.entities == set()
assert info.all_states is True
assert info.rate_limit == template.DEFAULT_RATE_LIMIT
@@ -1643,13 +1647,7 @@ async def test_async_render_to_info_with_wildcard_matching_state(hass):
info = render_to_info(hass, template_complex_str)
assert not info.domains
assert info.entities == {
"cover.x_skylight",
"binary_sensor.door",
"cover.office_drapes",
"cover.office_window",
"cover.office_skylight",
}
assert info.entities == set()
assert info.all_states is True
assert info.rate_limit == template.DEFAULT_RATE_LIMIT
@@ -1666,12 +1664,7 @@ async def test_async_render_to_info_with_wildcard_matching_state(hass):
info = render_to_info(hass, template_cover_str)
assert info.domains == {"cover"}
assert info.entities == {
"cover.x_skylight",
"cover.office_drapes",
"cover.office_window",
"cover.office_skylight",
}
assert info.entities == set()
assert info.all_states is False
assert info.rate_limit == template.DEFAULT_RATE_LIMIT
@@ -1965,9 +1958,7 @@ def test_generate_filter_iterators(hass):
{% endfor %}
""",
)
assert_result_info(
info, "sensor.test_sensor=off,", ["sensor.test_sensor"], ["sensor"]
)
assert_result_info(info, "sensor.test_sensor=off,", [], ["sensor"])
info = render_to_info(
hass,
@@ -1977,9 +1968,7 @@ def test_generate_filter_iterators(hass):
{% endfor %}
""",
)
assert_result_info(
info, "sensor.test_sensor=value,", ["sensor.test_sensor"], ["sensor"]
)
assert_result_info(info, "sensor.test_sensor=value,", [], ["sensor"])
def test_generate_select(hass):
@@ -2001,7 +1990,7 @@ def test_generate_select(hass):
assert_result_info(
info,
"sensor.test_sensor",
["sensor.test_sensor", "sensor.test_sensor_on"],
[],
["sensor"],
)
assert info.domains_lifecycle == {"sensor"}
@@ -2542,12 +2531,24 @@ async def test_lights(hass):
tmp = template.Template(tmpl, hass)
info = tmp.async_render_to_info()
assert info.entities == set(states)
assert info.entities == set()
assert info.domains == {"light"}
assert "lights are on" in info.result()
for i in range(10):
assert f"sensor{i}" in info.result()
async def test_template_errors(hass):
"""Test template rendering wraps exceptions with TemplateError."""
with pytest.raises(TemplateError):
template.Template("{{ now() | rando }}", hass).async_render()
with pytest.raises(TemplateError):
template.Template("{{ now() | random }}", hass).async_render()
async def test_state_attributes(hass):
"""Test state attributes."""
hass.states.async_set("sensor.test", "23")

View File

@@ -369,6 +369,36 @@ async def test_loading_configuration_from_storage(hass, hass_storage):
assert hass.config.config_source == SOURCE_STORAGE
async def test_loading_configuration_from_storage_with_yaml_only(hass, hass_storage):
"""Test loading core and YAML config onto hass object."""
hass_storage["core.config"] = {
"data": {
"elevation": 10,
"latitude": 55,
"location_name": "Home",
"longitude": 13,
"time_zone": "Europe/Copenhagen",
"unit_system": "metric",
},
"key": "core.config",
"version": 1,
}
await config_util.async_process_ha_core_config(
hass, {"media_dirs": {"mymedia": "/usr"}, "allowlist_external_dirs": "/etc"}
)
assert hass.config.latitude == 55
assert hass.config.longitude == 13
assert hass.config.elevation == 10
assert hass.config.location_name == "Home"
assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC
assert hass.config.time_zone.zone == "Europe/Copenhagen"
assert len(hass.config.allowlist_external_dirs) == 3
assert "/etc" in hass.config.allowlist_external_dirs
assert hass.config.media_dirs == {"mymedia": "/usr"}
assert hass.config.config_source == SOURCE_STORAGE
async def test_updating_configuration(hass, hass_storage):
"""Test updating configuration stores the new configuration."""
core_data = {