forked from home-assistant/core
Merge branch 'dev' into rc
This commit is contained in:
@@ -756,7 +756,6 @@ omit =
|
|||||||
homeassistant/components/twentemilieu/sensor.py
|
homeassistant/components/twentemilieu/sensor.py
|
||||||
homeassistant/components/twilio_call/notify.py
|
homeassistant/components/twilio_call/notify.py
|
||||||
homeassistant/components/twilio_sms/notify.py
|
homeassistant/components/twilio_sms/notify.py
|
||||||
homeassistant/components/twitch/sensor.py
|
|
||||||
homeassistant/components/twitter/notify.py
|
homeassistant/components/twitter/notify.py
|
||||||
homeassistant/components/ubee/device_tracker.py
|
homeassistant/components/ubee/device_tracker.py
|
||||||
homeassistant/components/ubus/device_tracker.py
|
homeassistant/components/ubus/device_tracker.py
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
"""Provide methods to bootstrap a Home Assistant instance."""
|
"""Provide methods to bootstrap a Home Assistant instance."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
@@ -7,12 +8,14 @@ import sys
|
|||||||
from time import monotonic
|
from time import monotonic
|
||||||
from typing import Any, Dict, Optional, Set
|
from typing import Any, Dict, Optional, Set
|
||||||
|
|
||||||
|
from async_timeout import timeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config as conf_util, config_entries, core, loader
|
from homeassistant import config as conf_util, config_entries, core, loader
|
||||||
from homeassistant.components import http
|
from homeassistant.components import http
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_CLOSE,
|
EVENT_HOMEASSISTANT_CLOSE,
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
REQUIRED_NEXT_PYTHON_DATE,
|
REQUIRED_NEXT_PYTHON_DATE,
|
||||||
REQUIRED_NEXT_PYTHON_VER,
|
REQUIRED_NEXT_PYTHON_VER,
|
||||||
)
|
)
|
||||||
@@ -80,8 +83,7 @@ async def async_setup_hass(
|
|||||||
config_dict = await conf_util.async_hass_config_yaml(hass)
|
config_dict = await conf_util.async_hass_config_yaml(hass)
|
||||||
except HomeAssistantError as err:
|
except HomeAssistantError as err:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Failed to parse configuration.yaml: %s. Falling back to safe mode",
|
"Failed to parse configuration.yaml: %s. Activating safe mode", err,
|
||||||
err,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if not is_virtual_env():
|
if not is_virtual_env():
|
||||||
@@ -93,8 +95,30 @@ async def async_setup_hass(
|
|||||||
finally:
|
finally:
|
||||||
clear_secret_cache()
|
clear_secret_cache()
|
||||||
|
|
||||||
if safe_mode or config_dict is None or not basic_setup_success:
|
if config_dict is None:
|
||||||
|
safe_mode = True
|
||||||
|
|
||||||
|
elif not basic_setup_success:
|
||||||
|
_LOGGER.warning("Unable to set up core integrations. Activating safe mode")
|
||||||
|
safe_mode = True
|
||||||
|
|
||||||
|
elif "frontend" not in hass.config.components:
|
||||||
|
_LOGGER.warning("Detected that frontend did not load. Activating safe mode")
|
||||||
|
# Ask integrations to shut down. It's messy but we can't
|
||||||
|
# do a clean stop without knowing what is broken
|
||||||
|
hass.async_track_tasks()
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
|
||||||
|
with contextlib.suppress(asyncio.TimeoutError):
|
||||||
|
async with timeout(10):
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
safe_mode = True
|
||||||
|
hass = core.HomeAssistant()
|
||||||
|
hass.config.config_dir = config_dir
|
||||||
|
|
||||||
|
if safe_mode:
|
||||||
_LOGGER.info("Starting in safe mode")
|
_LOGGER.info("Starting in safe mode")
|
||||||
|
hass.config.safe_mode = True
|
||||||
|
|
||||||
http_conf = (await http.async_get_last_config(hass)) or {}
|
http_conf = (await http.async_get_last_config(hass)) or {}
|
||||||
|
|
||||||
@@ -283,7 +307,7 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
|
|||||||
domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN)
|
domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN)
|
||||||
|
|
||||||
# Add config entry domains
|
# Add config entry domains
|
||||||
if "safe_mode" not in config:
|
if not hass.config.safe_mode:
|
||||||
domains.update(hass.config_entries.async_domains())
|
domains.update(hass.config_entries.async_domains())
|
||||||
|
|
||||||
# Make sure the Hass.io component is loaded
|
# Make sure the Hass.io component is loaded
|
||||||
|
@@ -4,5 +4,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||||
"requirements": ["pyatv==0.3.13"],
|
"requirements": ["pyatv==0.3.13"],
|
||||||
"dependencies": ["configurator"],
|
"dependencies": ["configurator"],
|
||||||
|
"after_dependencies": ["discovery"],
|
||||||
"codeowners": []
|
"codeowners": []
|
||||||
}
|
}
|
||||||
|
@@ -225,6 +225,13 @@ class AugustData:
|
|||||||
self._door_state_by_id = {}
|
self._door_state_by_id = {}
|
||||||
self._activities_by_id = {}
|
self._activities_by_id = {}
|
||||||
|
|
||||||
|
# We check the locks right away so we can
|
||||||
|
# remove inoperative ones
|
||||||
|
self._update_locks_status()
|
||||||
|
self._update_locks_detail()
|
||||||
|
|
||||||
|
self._filter_inoperative_locks()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def house_ids(self):
|
def house_ids(self):
|
||||||
"""Return a list of house_ids."""
|
"""Return a list of house_ids."""
|
||||||
@@ -352,6 +359,14 @@ class AugustData:
|
|||||||
self._lock_last_status_update_time_utc_by_id[lock_id] = update_start_time_utc
|
self._lock_last_status_update_time_utc_by_id[lock_id] = update_start_time_utc
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def lock_has_doorsense(self, lock_id):
|
||||||
|
"""Determine if a lock has doorsense installed and can tell when the door is open or closed."""
|
||||||
|
# We do not update here since this is not expected
|
||||||
|
# to change until restart
|
||||||
|
if self._lock_detail_by_id[lock_id] is None:
|
||||||
|
return False
|
||||||
|
return self._lock_detail_by_id[lock_id].doorsense
|
||||||
|
|
||||||
async def async_get_lock_status(self, lock_id):
|
async def async_get_lock_status(self, lock_id):
|
||||||
"""Return status if the door is locked or unlocked.
|
"""Return status if the door is locked or unlocked.
|
||||||
|
|
||||||
@@ -497,6 +512,33 @@ class AugustData:
|
|||||||
device_id,
|
device_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _filter_inoperative_locks(self):
|
||||||
|
# Remove non-operative locks as there must
|
||||||
|
# be a bridge (August Connect) for them to
|
||||||
|
# be usable
|
||||||
|
operative_locks = []
|
||||||
|
for lock in self._locks:
|
||||||
|
lock_detail = self._lock_detail_by_id.get(lock.device_id)
|
||||||
|
if lock_detail is None:
|
||||||
|
_LOGGER.info(
|
||||||
|
"The lock %s could not be setup because the system could not fetch details about the lock.",
|
||||||
|
lock.device_name,
|
||||||
|
)
|
||||||
|
elif lock_detail.bridge is None:
|
||||||
|
_LOGGER.info(
|
||||||
|
"The lock %s could not be setup because it does not have a bridge (Connect).",
|
||||||
|
lock.device_name,
|
||||||
|
)
|
||||||
|
elif not lock_detail.bridge.operative:
|
||||||
|
_LOGGER.info(
|
||||||
|
"The lock %s could not be setup because the bridge (Connect) is not operative.",
|
||||||
|
lock.device_name,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
operative_locks.append(lock)
|
||||||
|
|
||||||
|
self._locks = operative_locks
|
||||||
|
|
||||||
|
|
||||||
def _call_api_operation_that_requires_bridge(
|
def _call_api_operation_that_requires_bridge(
|
||||||
device_name, operation_name, func, *args, **kwargs
|
device_name, operation_name, func, *args, **kwargs
|
||||||
|
@@ -12,7 +12,7 @@ from . import DATA_AUGUST
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=10)
|
SCAN_INTERVAL = timedelta(seconds=5)
|
||||||
|
|
||||||
|
|
||||||
async def _async_retrieve_door_state(data, lock):
|
async def _async_retrieve_door_state(data, lock):
|
||||||
@@ -51,11 +51,15 @@ async def _async_activity_time_based_state(data, doorbell, activity_types):
|
|||||||
|
|
||||||
if latest is not None:
|
if latest is not None:
|
||||||
start = latest.activity_start_time
|
start = latest.activity_start_time
|
||||||
end = latest.activity_end_time + timedelta(seconds=30)
|
end = latest.activity_end_time + timedelta(seconds=45)
|
||||||
return start <= datetime.now() <= end
|
return start <= datetime.now() <= end
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
SENSOR_NAME = 0
|
||||||
|
SENSOR_DEVICE_CLASS = 1
|
||||||
|
SENSOR_STATE_PROVIDER = 2
|
||||||
|
|
||||||
# sensor_type: [name, device_class, async_state_provider]
|
# sensor_type: [name, device_class, async_state_provider]
|
||||||
SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _async_retrieve_door_state]}
|
SENSOR_TYPES_DOOR = {"door_open": ["Open", "door", _async_retrieve_door_state]}
|
||||||
|
|
||||||
@@ -73,18 +77,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
|
|
||||||
for door in data.locks:
|
for door in data.locks:
|
||||||
for sensor_type in SENSOR_TYPES_DOOR:
|
for sensor_type in SENSOR_TYPES_DOOR:
|
||||||
async_state_provider = SENSOR_TYPES_DOOR[sensor_type][2]
|
if not data.lock_has_doorsense(door.device_id):
|
||||||
if await async_state_provider(data, door) is LockDoorStatus.UNKNOWN:
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Not adding sensor class %s for lock %s ",
|
"Not adding sensor class %s for lock %s ",
|
||||||
SENSOR_TYPES_DOOR[sensor_type][1],
|
SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
|
||||||
door.device_name,
|
door.device_name,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Adding sensor class %s for %s",
|
"Adding sensor class %s for %s",
|
||||||
SENSOR_TYPES_DOOR[sensor_type][1],
|
SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
|
||||||
door.device_name,
|
door.device_name,
|
||||||
)
|
)
|
||||||
devices.append(AugustDoorBinarySensor(data, sensor_type, door))
|
devices.append(AugustDoorBinarySensor(data, sensor_type, door))
|
||||||
@@ -93,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
|||||||
for sensor_type in SENSOR_TYPES_DOORBELL:
|
for sensor_type in SENSOR_TYPES_DOORBELL:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Adding doorbell sensor class %s for %s",
|
"Adding doorbell sensor class %s for %s",
|
||||||
SENSOR_TYPES_DOORBELL[sensor_type][1],
|
SENSOR_TYPES_DOORBELL[sensor_type][SENSOR_DEVICE_CLASS],
|
||||||
doorbell.device_name,
|
doorbell.device_name,
|
||||||
)
|
)
|
||||||
devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell))
|
devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell))
|
||||||
@@ -125,22 +128,25 @@ class AugustDoorBinarySensor(BinarySensorDevice):
|
|||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||||
return SENSOR_TYPES_DOOR[self._sensor_type][1]
|
return SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_DEVICE_CLASS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the binary sensor."""
|
"""Return the name of the binary sensor."""
|
||||||
return "{} {}".format(
|
return "{} {}".format(
|
||||||
self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][0]
|
self._door.device_name, SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME]
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Get the latest state of the sensor and update activity."""
|
"""Get the latest state of the sensor and update activity."""
|
||||||
async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2]
|
async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][
|
||||||
self._state = await async_state_provider(self._data, self._door)
|
SENSOR_STATE_PROVIDER
|
||||||
self._available = self._state is not None
|
]
|
||||||
|
lock_door_state = await async_state_provider(self._data, self._door)
|
||||||
self._state = self._state == LockDoorStatus.OPEN
|
self._available = (
|
||||||
|
lock_door_state is not None and lock_door_state != LockDoorStatus.UNKNOWN
|
||||||
|
)
|
||||||
|
self._state = lock_door_state == LockDoorStatus.OPEN
|
||||||
|
|
||||||
door_activity = await self._data.async_get_latest_device_activity(
|
door_activity = await self._data.async_get_latest_device_activity(
|
||||||
self._door.device_id, ActivityType.DOOR_OPERATION
|
self._door.device_id, ActivityType.DOOR_OPERATION
|
||||||
@@ -193,7 +199,8 @@ class AugustDoorBinarySensor(BinarySensorDevice):
|
|||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
"""Get the unique of the door open binary sensor."""
|
"""Get the unique of the door open binary sensor."""
|
||||||
return "{:s}_{:s}".format(
|
return "{:s}_{:s}".format(
|
||||||
self._door.device_id, SENSOR_TYPES_DOOR[self._sensor_type][0].lower()
|
self._door.device_id,
|
||||||
|
SENSOR_TYPES_DOOR[self._sensor_type][SENSOR_NAME].lower(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -221,25 +228,31 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
|
|||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||||
return SENSOR_TYPES_DOORBELL[self._sensor_type][1]
|
return SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_DEVICE_CLASS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the binary sensor."""
|
"""Return the name of the binary sensor."""
|
||||||
return "{} {}".format(
|
return "{} {}".format(
|
||||||
self._doorbell.device_name, SENSOR_TYPES_DOORBELL[self._sensor_type][0]
|
self._doorbell.device_name,
|
||||||
|
SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Get the latest state of the sensor."""
|
"""Get the latest state of the sensor."""
|
||||||
async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2]
|
async_state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][
|
||||||
|
SENSOR_STATE_PROVIDER
|
||||||
|
]
|
||||||
self._state = await async_state_provider(self._data, self._doorbell)
|
self._state = await async_state_provider(self._data, self._doorbell)
|
||||||
self._available = self._doorbell.is_online
|
# The doorbell will go into standby mode when there is no motion
|
||||||
|
# for a short while. It will wake by itself when needed so we need
|
||||||
|
# to consider is available or we will not report motion or dings
|
||||||
|
self._available = self._doorbell.is_online or self._doorbell.status == "standby"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
"""Get the unique id of the doorbell sensor."""
|
"""Get the unique id of the doorbell sensor."""
|
||||||
return "{:s}_{:s}".format(
|
return "{:s}_{:s}".format(
|
||||||
self._doorbell.device_id,
|
self._doorbell.device_id,
|
||||||
SENSOR_TYPES_DOORBELL[self._sensor_type][0].lower(),
|
SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower(),
|
||||||
)
|
)
|
||||||
|
@@ -67,7 +67,9 @@ class AugustLock(LockDevice):
|
|||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
"""Get the latest state of the sensor and update activity."""
|
"""Get the latest state of the sensor and update activity."""
|
||||||
self._lock_status = await self._data.async_get_lock_status(self._lock.device_id)
|
self._lock_status = await self._data.async_get_lock_status(self._lock.device_id)
|
||||||
self._available = self._lock_status is not None
|
self._available = (
|
||||||
|
self._lock_status is not None and self._lock_status != LockStatus.UNKNOWN
|
||||||
|
)
|
||||||
self._lock_detail = await self._data.async_get_lock_detail(self._lock.device_id)
|
self._lock_detail = await self._data.async_get_lock_detail(self._lock.device_id)
|
||||||
|
|
||||||
lock_activity = await self._data.async_get_latest_device_activity(
|
lock_activity = await self._data.async_get_latest_device_activity(
|
||||||
|
@@ -11,8 +11,10 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
|
|
||||||
CONF_ENCODING = "encoding"
|
CONF_ENCODING = "encoding"
|
||||||
|
CONF_QOS = "qos"
|
||||||
CONF_TOPIC = "topic"
|
CONF_TOPIC = "topic"
|
||||||
DEFAULT_ENCODING = "utf-8"
|
DEFAULT_ENCODING = "utf-8"
|
||||||
|
DEFAULT_QOS = 0
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema(
|
TRIGGER_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@@ -20,6 +22,9 @@ TRIGGER_SCHEMA = vol.Schema(
|
|||||||
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
|
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
|
||||||
vol.Optional(CONF_PAYLOAD): cv.string,
|
vol.Optional(CONF_PAYLOAD): cv.string,
|
||||||
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
|
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
|
||||||
|
vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All(
|
||||||
|
vol.Coerce(int), vol.In([0, 1, 2])
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,6 +34,7 @@ async def async_attach_trigger(hass, config, action, automation_info):
|
|||||||
topic = config[CONF_TOPIC]
|
topic = config[CONF_TOPIC]
|
||||||
payload = config.get(CONF_PAYLOAD)
|
payload = config.get(CONF_PAYLOAD)
|
||||||
encoding = config[CONF_ENCODING] or None
|
encoding = config[CONF_ENCODING] or None
|
||||||
|
qos = config[CONF_QOS]
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def mqtt_automation_listener(mqttmsg):
|
def mqtt_automation_listener(mqttmsg):
|
||||||
@@ -49,6 +55,6 @@ async def async_attach_trigger(hass, config, action, automation_info):
|
|||||||
hass.async_run_job(action, {"trigger": data})
|
hass.async_run_job(action, {"trigger": data})
|
||||||
|
|
||||||
remove = await mqtt.async_subscribe(
|
remove = await mqtt.async_subscribe(
|
||||||
hass, topic, mqtt_automation_listener, encoding=encoding
|
hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos
|
||||||
)
|
)
|
||||||
return remove
|
return remove
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "Enheten er allerede konfigurert",
|
"already_configured": "Enheten er allerede konfigurert",
|
||||||
"bad_config_file": "D\u00e5rlig data fra konfigurasjonsfilen",
|
"bad_config_file": "D\u00e5rlige data fra konfigurasjonsfilen",
|
||||||
"link_local_address": "Linking av lokale adresser st\u00f8ttes ikke",
|
"link_local_address": "Linking av lokale adresser st\u00f8ttes ikke",
|
||||||
"not_axis_device": "Oppdaget enhet ikke en Axis enhet",
|
"not_axis_device": "Oppdaget enhet ikke en Axis enhet",
|
||||||
"updated_configuration": "Oppdatert enhetskonfigurasjonen med ny vertsadresse"
|
"updated_configuration": "Oppdatert enhetskonfigurasjonen med ny vertsadresse"
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "Enheten \u00e4r redan konfigurerad",
|
"already_configured": "Enheten \u00e4r redan konfigurerad",
|
||||||
"bad_config_file": "Felaktig data fr\u00e5n config fil",
|
"bad_config_file": "Felaktig data fr\u00e5n konfigurationsfilen",
|
||||||
"link_local_address": "Link local addresses are not supported",
|
"link_local_address": "Link local addresses are not supported",
|
||||||
"not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet",
|
"not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet",
|
||||||
"updated_configuration": "Uppdaterad enhetskonfiguration med ny v\u00e4rdadress"
|
"updated_configuration": "Uppdaterad enhetskonfiguration med ny v\u00e4rdadress"
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"domain": "bmw_connected_drive",
|
"domain": "bmw_connected_drive",
|
||||||
"name": "BMW Connected Drive",
|
"name": "BMW Connected Drive",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||||
"requirements": ["bimmer_connected==0.7.0"],
|
"requirements": ["bimmer_connected==0.7.1"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@gerard33"]
|
"codeowners": ["@gerard33"]
|
||||||
}
|
}
|
||||||
|
@@ -108,7 +108,8 @@
|
|||||||
"allow_clip_sensor": "Allow deCONZ CLIP sensors",
|
"allow_clip_sensor": "Allow deCONZ CLIP sensors",
|
||||||
"allow_deconz_groups": "Allow deCONZ light groups"
|
"allow_deconz_groups": "Allow deCONZ light groups"
|
||||||
},
|
},
|
||||||
"description": "Configure visibility of deCONZ device types"
|
"description": "Configure visibility of deCONZ device types",
|
||||||
|
"title": "deCONZ options"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -37,6 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
gateway.option_allow_clip_sensor
|
gateway.option_allow_clip_sensor
|
||||||
or not sensor.type.startswith("CLIP")
|
or not sensor.type.startswith("CLIP")
|
||||||
)
|
)
|
||||||
|
and sensor.deconz_id not in gateway.deconz_ids.values()
|
||||||
):
|
):
|
||||||
entities.append(DeconzBinarySensor(sensor, gateway))
|
entities.append(DeconzBinarySensor(sensor, gateway))
|
||||||
|
|
||||||
|
@@ -44,6 +44,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
gateway.option_allow_clip_sensor
|
gateway.option_allow_clip_sensor
|
||||||
or not sensor.type.startswith("CLIP")
|
or not sensor.type.startswith("CLIP")
|
||||||
)
|
)
|
||||||
|
and sensor.deconz_id not in gateway.deconz_ids.values()
|
||||||
):
|
):
|
||||||
entities.append(DeconzThermostat(sensor, gateway))
|
entities.append(DeconzThermostat(sensor, gateway))
|
||||||
|
|
||||||
|
@@ -77,6 +77,11 @@ class DeconzDevice(DeconzBase, Entity):
|
|||||||
self.hass, self.gateway.signal_reachable, self.async_update_callback
|
self.hass, self.gateway.signal_reachable, self.async_update_callback
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.listeners.append(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass, self.gateway.signal_remove_entity, self.async_remove_self
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""Disconnect device object when removed."""
|
"""Disconnect device object when removed."""
|
||||||
@@ -85,6 +90,15 @@ class DeconzDevice(DeconzBase, Entity):
|
|||||||
for unsub_dispatcher in self.listeners:
|
for unsub_dispatcher in self.listeners:
|
||||||
unsub_dispatcher()
|
unsub_dispatcher()
|
||||||
|
|
||||||
|
async def async_remove_self(self, deconz_ids: list) -> None:
|
||||||
|
"""Schedule removal of this entity.
|
||||||
|
|
||||||
|
Called by signal_remove_entity scheduled by async_added_to_hass.
|
||||||
|
"""
|
||||||
|
if self._device.deconz_id not in deconz_ids:
|
||||||
|
return
|
||||||
|
await self.async_remove()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_update_callback(self, force_update=False, ignore_update=False):
|
def async_update_callback(self, force_update=False, ignore_update=False):
|
||||||
"""Update the device's state."""
|
"""Update the device's state."""
|
||||||
|
@@ -20,6 +20,8 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
NEW_DEVICE,
|
NEW_DEVICE,
|
||||||
|
NEW_GROUP,
|
||||||
|
NEW_SENSOR,
|
||||||
SUPPORTED_PLATFORMS,
|
SUPPORTED_PLATFORMS,
|
||||||
)
|
)
|
||||||
from .errors import AuthenticationRequired, CannotConnect
|
from .errors import AuthenticationRequired, CannotConnect
|
||||||
@@ -45,6 +47,9 @@ class DeconzGateway:
|
|||||||
self.events = []
|
self.events = []
|
||||||
self.listeners = []
|
self.listeners = []
|
||||||
|
|
||||||
|
self._current_option_allow_clip_sensor = self.option_allow_clip_sensor
|
||||||
|
self._current_option_allow_deconz_groups = self.option_allow_deconz_groups
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bridgeid(self) -> str:
|
def bridgeid(self) -> str:
|
||||||
"""Return the unique identifier of the gateway."""
|
"""Return the unique identifier of the gateway."""
|
||||||
@@ -108,22 +113,64 @@ class DeconzGateway:
|
|||||||
|
|
||||||
self.api.start()
|
self.api.start()
|
||||||
|
|
||||||
self.config_entry.add_update_listener(self.async_new_address)
|
self.config_entry.add_update_listener(self.async_config_entry_updated)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def async_new_address(hass, entry) -> None:
|
async def async_config_entry_updated(hass, entry) -> None:
|
||||||
"""Handle signals of gateway getting new address.
|
"""Handle signals of config entry being updated.
|
||||||
|
|
||||||
This is a static method because a class method (bound method),
|
This is a static method because a class method (bound method), can not be used with weak references.
|
||||||
can not be used with weak references.
|
Causes for this is either discovery updating host address or config entry options changing.
|
||||||
"""
|
"""
|
||||||
gateway = get_gateway_from_config_entry(hass, entry)
|
gateway = get_gateway_from_config_entry(hass, entry)
|
||||||
if gateway.api.host != entry.data[CONF_HOST]:
|
if gateway.api.host != entry.data[CONF_HOST]:
|
||||||
gateway.api.close()
|
gateway.api.close()
|
||||||
gateway.api.host = entry.data[CONF_HOST]
|
gateway.api.host = entry.data[CONF_HOST]
|
||||||
gateway.api.start()
|
gateway.api.start()
|
||||||
|
return
|
||||||
|
|
||||||
|
await gateway.options_updated()
|
||||||
|
|
||||||
|
async def options_updated(self):
|
||||||
|
"""Manage entities affected by config entry options."""
|
||||||
|
deconz_ids = []
|
||||||
|
|
||||||
|
if self._current_option_allow_clip_sensor != self.option_allow_clip_sensor:
|
||||||
|
self._current_option_allow_clip_sensor = self.option_allow_clip_sensor
|
||||||
|
|
||||||
|
sensors = [
|
||||||
|
sensor
|
||||||
|
for sensor in self.api.sensors.values()
|
||||||
|
if sensor.type.startswith("CLIP")
|
||||||
|
]
|
||||||
|
|
||||||
|
if self.option_allow_clip_sensor:
|
||||||
|
self.async_add_device_callback(NEW_SENSOR, sensors)
|
||||||
|
else:
|
||||||
|
deconz_ids += [sensor.deconz_id for sensor in sensors]
|
||||||
|
|
||||||
|
if self._current_option_allow_deconz_groups != self.option_allow_deconz_groups:
|
||||||
|
self._current_option_allow_deconz_groups = self.option_allow_deconz_groups
|
||||||
|
|
||||||
|
groups = list(self.api.groups.values())
|
||||||
|
|
||||||
|
if self.option_allow_deconz_groups:
|
||||||
|
self.async_add_device_callback(NEW_GROUP, groups)
|
||||||
|
else:
|
||||||
|
deconz_ids += [group.deconz_id for group in groups]
|
||||||
|
|
||||||
|
if deconz_ids:
|
||||||
|
async_dispatcher_send(self.hass, self.signal_remove_entity, deconz_ids)
|
||||||
|
|
||||||
|
entity_registry = await self.hass.helpers.entity_registry.async_get_registry()
|
||||||
|
|
||||||
|
for entity_id, deconz_id in self.deconz_ids.items():
|
||||||
|
if deconz_id in deconz_ids and entity_registry.async_is_registered(
|
||||||
|
entity_id
|
||||||
|
):
|
||||||
|
entity_registry.async_remove(entity_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def signal_reachable(self) -> str:
|
def signal_reachable(self) -> str:
|
||||||
@@ -141,6 +188,11 @@ class DeconzGateway:
|
|||||||
"""Gateway specific event to signal new device."""
|
"""Gateway specific event to signal new device."""
|
||||||
return NEW_DEVICE[device_type].format(self.bridgeid)
|
return NEW_DEVICE[device_type].format(self.bridgeid)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def signal_remove_entity(self) -> str:
|
||||||
|
"""Gateway specific event to signal removal of entity."""
|
||||||
|
return f"deconz-remove-{self.bridgeid}"
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_add_device_callback(self, device_type, device) -> None:
|
def async_add_device_callback(self, device_type, device) -> None:
|
||||||
"""Handle event of new device creation in deCONZ."""
|
"""Handle event of new device creation in deCONZ."""
|
||||||
|
@@ -67,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
entities = []
|
entities = []
|
||||||
|
|
||||||
for group in groups:
|
for group in groups:
|
||||||
if group.lights:
|
if group.lights and group.deconz_id not in gateway.deconz_ids.values():
|
||||||
entities.append(DeconzGroup(group, gateway))
|
entities.append(DeconzGroup(group, gateway))
|
||||||
|
|
||||||
async_add_entities(entities, True)
|
async_add_entities(entities, True)
|
||||||
|
@@ -68,6 +68,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
gateway.option_allow_clip_sensor
|
gateway.option_allow_clip_sensor
|
||||||
or not sensor.type.startswith("CLIP")
|
or not sensor.type.startswith("CLIP")
|
||||||
)
|
)
|
||||||
|
and sensor.deconz_id not in gateway.deconz_ids.values()
|
||||||
):
|
):
|
||||||
entities.append(DeconzSensor(sensor, gateway))
|
entities.append(DeconzSensor(sensor, gateway))
|
||||||
|
|
||||||
|
@@ -14,20 +14,9 @@
|
|||||||
"title": "Link with deCONZ",
|
"title": "Link with deCONZ",
|
||||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button"
|
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button"
|
||||||
},
|
},
|
||||||
"options": {
|
|
||||||
"title": "Extra configuration options for deCONZ",
|
|
||||||
"data": {
|
|
||||||
"allow_clip_sensor": "Allow importing virtual sensors",
|
|
||||||
"allow_deconz_groups": "Allow importing deCONZ groups"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"hassio_confirm": {
|
"hassio_confirm": {
|
||||||
"title": "deCONZ Zigbee gateway via Hass.io add-on",
|
"title": "deCONZ Zigbee gateway via Hass.io add-on",
|
||||||
"description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?",
|
"description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?"
|
||||||
"data": {
|
|
||||||
"allow_clip_sensor": "Allow importing virtual sensors",
|
|
||||||
"allow_deconz_groups": "Allow importing deCONZ groups"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -45,11 +34,12 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"deconz_devices": {
|
"deconz_devices": {
|
||||||
"description": "Configure visibility of deCONZ device types",
|
|
||||||
"data": {
|
"data": {
|
||||||
"allow_clip_sensor": "Allow deCONZ CLIP sensors",
|
"allow_clip_sensor": "Allow deCONZ CLIP sensors",
|
||||||
"allow_deconz_groups": "Allow deCONZ light groups"
|
"allow_deconz_groups": "Allow deCONZ light groups"
|
||||||
}
|
},
|
||||||
|
"description": "Configure visibility of deCONZ device types",
|
||||||
|
"title": "deCONZ options"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -1,5 +1,22 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"title": "Demostraci\u00f3"
|
"title": "Demostraci\u00f3"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"options_1": {
|
||||||
|
"data": {
|
||||||
|
"bool": "Entrada booleana opcional",
|
||||||
|
"int": "Entrada num\u00e8rica"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options_2": {
|
||||||
|
"data": {
|
||||||
|
"multi": "Selecci\u00f3 m\u00faltiple",
|
||||||
|
"select": "Selecciona una opci\u00f3",
|
||||||
|
"string": "Valor de cadena (string)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,22 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"title": "Demo"
|
"title": "Demo"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"options_1": {
|
||||||
|
"data": {
|
||||||
|
"bool": "Optional boolean",
|
||||||
|
"int": "Numeric input"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options_2": {
|
||||||
|
"data": {
|
||||||
|
"multi": "Multiselect",
|
||||||
|
"select": "Select an option",
|
||||||
|
"string": "String value"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,28 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"title": "Demo"
|
"title": "Demo"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"one": "uno",
|
||||||
|
"other": "altro"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options_1": {
|
||||||
|
"data": {
|
||||||
|
"bool": "Valore booleano facoltativo",
|
||||||
|
"int": "Input numerico"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options_2": {
|
||||||
|
"data": {
|
||||||
|
"multi": "Selezione multipla",
|
||||||
|
"select": "Selezionare un'opzione",
|
||||||
|
"string": "Valore stringa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,28 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"title": "Demo"
|
"title": "Demo"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"one": "Empty",
|
||||||
|
"other": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options_1": {
|
||||||
|
"data": {
|
||||||
|
"bool": "Optioneel Boolean",
|
||||||
|
"int": "Numerieke invoer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options_2": {
|
||||||
|
"data": {
|
||||||
|
"multi": "Meerkeuze selectie",
|
||||||
|
"select": "Kies een optie",
|
||||||
|
"string": "String waarde"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,30 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"title": "Demo"
|
"title": "Demo"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"few": "prazni",
|
||||||
|
"one": "prazen",
|
||||||
|
"other": "prazni",
|
||||||
|
"two": "prazna"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options_1": {
|
||||||
|
"data": {
|
||||||
|
"bool": "Izbirna logi\u010dna vrednost",
|
||||||
|
"int": "\u0160tevil\u010dni vnos"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options_2": {
|
||||||
|
"data": {
|
||||||
|
"multi": "Multiselect",
|
||||||
|
"select": "Izberite mo\u017enost",
|
||||||
|
"string": "Vrednost niza"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,28 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"title": "Demo"
|
"title": "Demo"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"one": "Tom",
|
||||||
|
"other": "Tomma"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options_1": {
|
||||||
|
"data": {
|
||||||
|
"bool": "Valfritt boolesk",
|
||||||
|
"int": "Numerisk inmatning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options_2": {
|
||||||
|
"data": {
|
||||||
|
"multi": "Flera val",
|
||||||
|
"select": "V\u00e4lj ett alternativ",
|
||||||
|
"string": "Str\u00e4ngv\u00e4rde"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,22 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"title": "\u5c55\u793a"
|
"title": "\u5c55\u793a"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"options_1": {
|
||||||
|
"data": {
|
||||||
|
"bool": "\u9078\u9805\u5e03\u6797",
|
||||||
|
"int": "\u6578\u503c\u8f38\u5165"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options_2": {
|
||||||
|
"data": {
|
||||||
|
"multi": "\u591a\u91cd\u9078\u64c7",
|
||||||
|
"select": "\u9078\u64c7\u9078\u9805",
|
||||||
|
"string": "\u5b57\u4e32\u503c"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -4,5 +4,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/freebox",
|
"documentation": "https://www.home-assistant.io/integrations/freebox",
|
||||||
"requirements": ["aiofreepybox==0.0.8"],
|
"requirements": ["aiofreepybox==0.0.8"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
|
"after_dependencies": ["discovery"],
|
||||||
"codeowners": ["@snoof85"]
|
"codeowners": ["@snoof85"]
|
||||||
}
|
}
|
||||||
|
@@ -508,6 +508,23 @@ def websocket_get_themes(hass, connection, msg):
|
|||||||
|
|
||||||
Async friendly.
|
Async friendly.
|
||||||
"""
|
"""
|
||||||
|
if hass.config.safe_mode:
|
||||||
|
connection.send_message(
|
||||||
|
websocket_api.result_message(
|
||||||
|
msg["id"],
|
||||||
|
{
|
||||||
|
"themes": {
|
||||||
|
"safe_mode": {
|
||||||
|
"primary-color": "#db4437",
|
||||||
|
"accent-color": "#eeee02",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default_theme": "safe_mode",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
connection.send_message(
|
connection.send_message(
|
||||||
websocket_api.result_message(
|
websocket_api.result_message(
|
||||||
msg["id"],
|
msg["id"],
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
"name": "Home Assistant Frontend",
|
"name": "Home Assistant Frontend",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"home-assistant-frontend==20200217.0"
|
"home-assistant-frontend==20200219.0"
|
||||||
],
|
],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"api",
|
"api",
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
"name": "GeoNet NZ Quakes",
|
"name": "GeoNet NZ Quakes",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes",
|
"documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes",
|
||||||
"requirements": ["aio_geojson_geonetnz_quakes==0.11"],
|
"requirements": ["aio_geojson_geonetnz_quakes==0.12"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@exxamalte"]
|
"codeowners": ["@exxamalte"]
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
"already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.",
|
"already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.",
|
||||||
"already_paired": "Dette tilbeh\u00f8ret er allerede sammenkoblet med en annen enhet. Vennligst tilbakestill tilbeh\u00f8ret og pr\u00f8v igjen.",
|
"already_paired": "Dette tilbeh\u00f8ret er allerede sammenkoblet med en annen enhet. Vennligst tilbakestill tilbeh\u00f8ret og pr\u00f8v igjen.",
|
||||||
"ignored_model": "HomeKit st\u00f8tte for denne modellen er blokkert da en mer funksjonsrik standard integrering er tilgjengelig.",
|
"ignored_model": "HomeKit st\u00f8tte for denne modellen er blokkert da en mer funksjonsrik standard integrering er tilgjengelig.",
|
||||||
"invalid_config_entry": "Denne enheten vises som klar til \u00e5 sammenkoble, men det er allerede en motstridende konfigurasjonsoppf\u00f8ring for den i Home Assistant som m\u00e5 fjernes f\u00f8rst.",
|
"invalid_config_entry": "Denne enheten vises som klar til sammenkobling, men det er allerede en motstridende konfigurasjonsoppf\u00f8ring for den i Hjelpeassistenten som f\u00f8rst m\u00e5 fjernes.",
|
||||||
"no_devices": "Ingen ukoblede enheter ble funnet"
|
"no_devices": "Ingen ukoblede enheter ble funnet"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
@@ -7,6 +7,7 @@ from homematicip.aio.device import (
|
|||||||
AsyncFullFlushShutter,
|
AsyncFullFlushShutter,
|
||||||
AsyncGarageDoorModuleTormatic,
|
AsyncGarageDoorModuleTormatic,
|
||||||
)
|
)
|
||||||
|
from homematicip.aio.group import AsyncExtendedLinkedShutterGroup
|
||||||
from homematicip.base.enums import DoorCommand, DoorState
|
from homematicip.base.enums import DoorCommand, DoorState
|
||||||
|
|
||||||
from homeassistant.components.cover import (
|
from homeassistant.components.cover import (
|
||||||
@@ -18,6 +19,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice
|
from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice
|
||||||
|
from .hap import HomematicipHAP
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -41,6 +43,10 @@ async def async_setup_entry(
|
|||||||
elif isinstance(device, AsyncGarageDoorModuleTormatic):
|
elif isinstance(device, AsyncGarageDoorModuleTormatic):
|
||||||
entities.append(HomematicipGarageDoorModuleTormatic(hap, device))
|
entities.append(HomematicipGarageDoorModuleTormatic(hap, device))
|
||||||
|
|
||||||
|
for group in hap.home.groups:
|
||||||
|
if isinstance(group, AsyncExtendedLinkedShutterGroup):
|
||||||
|
entities.append(HomematicipCoverShutterGroup(hap, group))
|
||||||
|
|
||||||
if entities:
|
if entities:
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
@@ -142,3 +148,12 @@ class HomematicipGarageDoorModuleTormatic(HomematicipGenericDevice, CoverDevice)
|
|||||||
async def async_stop_cover(self, **kwargs) -> None:
|
async def async_stop_cover(self, **kwargs) -> None:
|
||||||
"""Stop the cover."""
|
"""Stop the cover."""
|
||||||
await self._device.send_door_command(DoorCommand.STOP)
|
await self._device.send_door_command(DoorCommand.STOP)
|
||||||
|
|
||||||
|
|
||||||
|
class HomematicipCoverShutterGroup(HomematicipCoverSlats, CoverDevice):
|
||||||
|
"""Representation of a HomematicIP Cloud cover shutter group."""
|
||||||
|
|
||||||
|
def __init__(self, hap: HomematicipHAP, device, post: str = "ShutterGroup") -> None:
|
||||||
|
"""Initialize switching group."""
|
||||||
|
device.modelType = f"HmIP-{post}"
|
||||||
|
super().__init__(hap, device, post)
|
||||||
|
@@ -14,6 +14,7 @@ from homematicip.aio.device import (
|
|||||||
AsyncPassageDetector,
|
AsyncPassageDetector,
|
||||||
AsyncPlugableSwitchMeasuring,
|
AsyncPlugableSwitchMeasuring,
|
||||||
AsyncPresenceDetectorIndoor,
|
AsyncPresenceDetectorIndoor,
|
||||||
|
AsyncRoomControlDeviceAnalog,
|
||||||
AsyncTemperatureHumiditySensorDisplay,
|
AsyncTemperatureHumiditySensorDisplay,
|
||||||
AsyncTemperatureHumiditySensorOutdoor,
|
AsyncTemperatureHumiditySensorOutdoor,
|
||||||
AsyncTemperatureHumiditySensorWithoutDisplay,
|
AsyncTemperatureHumiditySensorWithoutDisplay,
|
||||||
@@ -79,6 +80,8 @@ async def async_setup_entry(
|
|||||||
):
|
):
|
||||||
entities.append(HomematicipTemperatureSensor(hap, device))
|
entities.append(HomematicipTemperatureSensor(hap, device))
|
||||||
entities.append(HomematicipHumiditySensor(hap, device))
|
entities.append(HomematicipHumiditySensor(hap, device))
|
||||||
|
elif isinstance(device, (AsyncRoomControlDeviceAnalog,)):
|
||||||
|
entities.append(HomematicipTemperatureSensor(hap, device))
|
||||||
if isinstance(
|
if isinstance(
|
||||||
device,
|
device,
|
||||||
(
|
(
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"latitude": "Breedegrad",
|
"latitude": "Breedegrad",
|
||||||
"longitude": "L\u00e4ngegrad",
|
"longitude": "L\u00e4ngegrad",
|
||||||
|
"mode": "Modus",
|
||||||
"name": "Numm"
|
"name": "Numm"
|
||||||
},
|
},
|
||||||
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
|
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
"longitude": "Longitude",
|
"longitude": "Longitude",
|
||||||
|
"mode": "Mode",
|
||||||
"name": "Naam"
|
"name": "Naam"
|
||||||
},
|
},
|
||||||
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
|
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"latitude": "Zemljepisna \u0161irina",
|
"latitude": "Zemljepisna \u0161irina",
|
||||||
"longitude": "Zemljepisna dol\u017eina",
|
"longitude": "Zemljepisna dol\u017eina",
|
||||||
|
"mode": "Na\u010din",
|
||||||
"name": "Ime"
|
"name": "Ime"
|
||||||
},
|
},
|
||||||
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
|
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"latitude": "Latitud",
|
"latitude": "Latitud",
|
||||||
"longitude": "Longitud",
|
"longitude": "Longitud",
|
||||||
|
"mode": "L\u00e4ge",
|
||||||
"name": "Namn"
|
"name": "Namn"
|
||||||
},
|
},
|
||||||
"description": "Portugisiska institutet f\u00f6r hav och atmosf\u00e4ren",
|
"description": "Portugisiska institutet f\u00f6r hav och atmosf\u00e4ren",
|
||||||
|
@@ -29,6 +29,10 @@
|
|||||||
"abort": {
|
"abort": {
|
||||||
"not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto"
|
"not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto"
|
||||||
},
|
},
|
||||||
|
"error": {
|
||||||
|
"one": "uno",
|
||||||
|
"other": "altro"
|
||||||
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"options_binary": {
|
"options_binary": {
|
||||||
"data": {
|
"data": {
|
||||||
|
@@ -10,7 +10,16 @@
|
|||||||
"cannot_connect": "Kann sech net mam Konnected Panel um {host}:{port} verbannen"
|
"cannot_connect": "Kann sech net mam Konnected Panel um {host}:{port} verbannen"
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"description": "Modell: {model}\nHost: {host}\nPort: {port}\n\nDir k\u00ebnnt den I/O a Panel Verhaalen an de Konnected Alarm Panel Astellunge konfigur\u00e9ieren.",
|
||||||
|
"title": "Konnected Apparat parat"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "Konnected Apparat IP Adress",
|
||||||
|
"port": "Konnected Apparat Port"
|
||||||
|
},
|
||||||
|
"description": "Informatioune vum Konnected Panel aginn.",
|
||||||
"title": "Konnected Apparat entdecken"
|
"title": "Konnected Apparat entdecken"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -20,6 +29,10 @@
|
|||||||
"abort": {
|
"abort": {
|
||||||
"not_konn_panel": "Keen erkannten Konnected.io Apparat"
|
"not_konn_panel": "Keen erkannten Konnected.io Apparat"
|
||||||
},
|
},
|
||||||
|
"error": {
|
||||||
|
"one": "Ee",
|
||||||
|
"other": "M\u00e9i"
|
||||||
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"options_binary": {
|
"options_binary": {
|
||||||
"data": {
|
"data": {
|
||||||
@@ -27,13 +40,16 @@
|
|||||||
"name": "Numm (optional)",
|
"name": "Numm (optional)",
|
||||||
"type": "Typ vun Bin\u00e4re Sensor"
|
"type": "Typ vun Bin\u00e4re Sensor"
|
||||||
},
|
},
|
||||||
|
"description": "Wiel d'Optioune fir den bin\u00e4ren Sensor dee mat {zone} verbonnen ass",
|
||||||
"title": "Bin\u00e4re Sensor konfigur\u00e9ieren"
|
"title": "Bin\u00e4re Sensor konfigur\u00e9ieren"
|
||||||
},
|
},
|
||||||
"options_digital": {
|
"options_digital": {
|
||||||
"data": {
|
"data": {
|
||||||
"name": "Numm (optional)",
|
"name": "Numm (optional)",
|
||||||
|
"poll_interval": "Intervall vun den Offroen (Minutten) (optional)",
|
||||||
"type": "Typ vum Sensor"
|
"type": "Typ vum Sensor"
|
||||||
},
|
},
|
||||||
|
"description": "Wiel d'Optioune fir den digitale Sensor dee mat {zone} verbonnen ass",
|
||||||
"title": "Digitale Sensor konfigur\u00e9ieren"
|
"title": "Digitale Sensor konfigur\u00e9ieren"
|
||||||
},
|
},
|
||||||
"options_io": {
|
"options_io": {
|
||||||
@@ -47,6 +63,7 @@
|
|||||||
"7": "Zon 7",
|
"7": "Zon 7",
|
||||||
"out": "OUT"
|
"out": "OUT"
|
||||||
},
|
},
|
||||||
|
"description": "{model} um {host} entdeckt.\u00a0Wiel Basis Konfiguratioun vun den I/O hei dr\u00ebnner aus - ofh\u00e4ngeg vum I/O erlaabt et bin\u00e4r Sensoren (op / zou Kontakter), digital Sensoren (dht an ds18b20) oder schaltbar Ausgab. D\u00e9i detaill\u00e9iert Optioune k\u00ebnnen en an den n\u00e4chste Schr\u00ebtt konfigur\u00e9iert ginn.",
|
||||||
"title": "I/O konfigur\u00e9ieren"
|
"title": "I/O konfigur\u00e9ieren"
|
||||||
},
|
},
|
||||||
"options_io_ext": {
|
"options_io_ext": {
|
||||||
@@ -59,14 +76,26 @@
|
|||||||
"alarm1": "ALARM1",
|
"alarm1": "ALARM1",
|
||||||
"alarm2_out2": "OUT2/ALARM2",
|
"alarm2_out2": "OUT2/ALARM2",
|
||||||
"out1": "OUT1"
|
"out1": "OUT1"
|
||||||
}
|
},
|
||||||
|
"description": "Wiel d'Konfiguratioun vun de verbleiwenden I/O hei dr\u00ebnner. D\u00e9i detaill\u00e9iert Optioune k\u00ebnnen en an den n\u00e4chste Schr\u00ebtt konfigur\u00e9iert ginn.",
|
||||||
|
"title": "Erweiderten I/O konfigur\u00e9ieren"
|
||||||
|
},
|
||||||
|
"options_misc": {
|
||||||
|
"data": {
|
||||||
|
"blink": "Blink panel LED un wann Status \u00c4nnerung gesch\u00e9ckt g\u00ebtt"
|
||||||
|
},
|
||||||
|
"description": "Wielt w.e.g. dat gew\u00ebnschte Verhalen fir \u00c4re Panel aus",
|
||||||
|
"title": "Divers Optioune astellen"
|
||||||
},
|
},
|
||||||
"options_switch": {
|
"options_switch": {
|
||||||
"data": {
|
"data": {
|
||||||
"activation": "Ausgang wann un",
|
"activation": "Ausgang wann un",
|
||||||
"momentary": "Pulsatiounsdauer (ms) (optional)",
|
"momentary": "Pulsatiounsdauer (ms) (optional)",
|
||||||
"name": "Numm (optional)"
|
"name": "Numm (optional)",
|
||||||
|
"pause": "Pausen zw\u00ebscht den Impulser (ms) (optional)",
|
||||||
|
"repeat": "Unzuel vu Widderhuelungen (-1= onendlech) (optional)"
|
||||||
},
|
},
|
||||||
|
"description": "Wielt w.e.g. d'Ausgaboptiounen fir {zone}",
|
||||||
"title": "\u00cbmschltbaren Ausgang konfigur\u00e9ieren"
|
"title": "\u00cbmschltbaren Ausgang konfigur\u00e9ieren"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -1,14 +1,28 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Apparaat is al geconfigureerd",
|
||||||
|
"unknown": "Onbekende fout opgetreden"
|
||||||
|
},
|
||||||
"step": {
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"title": "Konnected Apparaat Klaar"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "IP-adres van Konnected apparaat"
|
"host": "IP-adres van Konnected apparaat",
|
||||||
}
|
"port": "Konnected apparaat poort"
|
||||||
}
|
},
|
||||||
|
"description": "Voer de host-informatie in voor uw Konnected-paneel.",
|
||||||
|
"title": "Ontdek Konnected Device"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"title": "Konnected.io"
|
||||||
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
"abort": {
|
||||||
|
"not_konn_panel": "Geen herkend Konnected.io apparaat"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"one": "Leeg",
|
"one": "Leeg",
|
||||||
"other": "Leeg"
|
"other": "Leeg"
|
||||||
@@ -16,8 +30,11 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"options_binary": {
|
"options_binary": {
|
||||||
"data": {
|
"data": {
|
||||||
"name": "Naam (optioneel)"
|
"inverse": "Keer de open / dicht status om",
|
||||||
}
|
"name": "Naam (optioneel)",
|
||||||
|
"type": "Type binaire sensor"
|
||||||
|
},
|
||||||
|
"title": "Binaire sensor configureren"
|
||||||
},
|
},
|
||||||
"options_digital": {
|
"options_digital": {
|
||||||
"data": {
|
"data": {
|
||||||
@@ -48,8 +65,10 @@
|
|||||||
"options_switch": {
|
"options_switch": {
|
||||||
"data": {
|
"data": {
|
||||||
"name": "Naam (optioneel)"
|
"name": "Naam (optioneel)"
|
||||||
}
|
},
|
||||||
}
|
"title": "Schakelbare uitgang configureren"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"title": "Konnected Alarm Paneel Opties"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -2,8 +2,27 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
|
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.",
|
||||||
|
"already_in_progress": "Konfiguracja urz\u0105dzenia jest ju\u017c w toku.",
|
||||||
|
"not_konn_panel": "Nie rozpoznano urz\u0105dzenia Konnected.io",
|
||||||
"unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d."
|
"unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d."
|
||||||
},
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z panelem Konnected na {host}:{port}"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"confirm": {
|
||||||
|
"description": "Model: {model} \nHost: {host} \nPort: {port} \n\nMo\u017cesz skonfigurowa\u0107 IO i zachowanie panelu w ustawieniach Konnected Alarm Panel.",
|
||||||
|
"title": "Urz\u0105dzenie Konnected gotowe"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "Adres IP urz\u0105dzenia Konnected",
|
||||||
|
"port": "Port urz\u0105dzenia Konnected urz\u0105dzenia"
|
||||||
|
},
|
||||||
|
"description": "Wprowad\u017a informacje o ho\u015bcie panelu Konnected.",
|
||||||
|
"title": "Wykryj urz\u0105dzenie Konnected"
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": "Konnected.io"
|
"title": "Konnected.io"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
@@ -31,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"one": "Tom",
|
"one": "Tom",
|
||||||
"other": "Tom"
|
"other": "Tomma"
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"options_binary": {
|
"options_binary": {
|
||||||
|
@@ -89,6 +89,9 @@ class LovelaceStorage:
|
|||||||
|
|
||||||
async def async_load(self, force):
|
async def async_load(self, force):
|
||||||
"""Load config."""
|
"""Load config."""
|
||||||
|
if self._hass.config.safe_mode:
|
||||||
|
raise ConfigNotFound
|
||||||
|
|
||||||
if self._data is None:
|
if self._data is None:
|
||||||
await self._load()
|
await self._load()
|
||||||
|
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "MELCloud Integratioun ass scho konfigur\u00e9iert fir d\u00ebs Email. Acc\u00e8s Jeton gouf erneiert."
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.",
|
"cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.",
|
||||||
"invalid_auth": "Ong\u00eblteg Authentifikatioun",
|
"invalid_auth": "Ong\u00eblteg Authentifikatioun",
|
||||||
@@ -8,8 +11,10 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"password": "MELCloud Passwuert"
|
"password": "MELCloud Passwuert",
|
||||||
|
"username": "Email d\u00e9i benotz g\u00ebtt fir sech mat MELCloud ze verbannen"
|
||||||
},
|
},
|
||||||
|
"description": "Verbann dech mat dengem MElCloud Kont.",
|
||||||
"title": "Mat MELCloud verbannen"
|
"title": "Mat MELCloud verbannen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
23
homeassistant/components/melcloud/.translations/nl.json
Normal file
23
homeassistant/components/melcloud/.translations/nl.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "MELCloud integratie is al geconfigureerd voor deze e-mail. Toegangstoken is vernieuwd."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Verbinding mislukt, probeer het opnieuw",
|
||||||
|
"invalid_auth": "Ongeldige authenticatie",
|
||||||
|
"unknown": "Onverwachte fout"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"password": "MELCloud wachtwoord.",
|
||||||
|
"username": "E-mail gebruikt om in te loggen op MELCloud."
|
||||||
|
},
|
||||||
|
"description": "Maak verbinding via uw MELCloud account.",
|
||||||
|
"title": "Maak verbinding met MELCloud"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "MELCloud"
|
||||||
|
}
|
||||||
|
}
|
@@ -8,6 +8,16 @@
|
|||||||
"invalid_auth": "Niepoprawne uwierzytelnienie.",
|
"invalid_auth": "Niepoprawne uwierzytelnienie.",
|
||||||
"unknown": "Niespodziewany b\u0142\u0105d."
|
"unknown": "Niespodziewany b\u0142\u0105d."
|
||||||
},
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"password": "Has\u0142o MELCloud.",
|
||||||
|
"username": "Adres e-mail u\u017cywany do logowania do MELCloud"
|
||||||
|
},
|
||||||
|
"description": "Po\u0142\u0105cz u\u017cywaj\u0105c swojego konta MELCloud.",
|
||||||
|
"title": "Po\u0142\u0105cz si\u0119 z MELCloud"
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": "MELCloud"
|
"title": "MELCloud"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -3,13 +3,20 @@
|
|||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "Apparat ass scho konfigur\u00e9iert"
|
"already_configured": "Apparat ass scho konfigur\u00e9iert"
|
||||||
},
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Feeler beim verbannen mam Server. Iwwerpr\u00e9if den Numm a Port a prob\u00e9ier nach emol. G\u00e9i och s\u00e9cher dass op d'mannst Minecraft Versioun 1.7 um Server leeft.",
|
||||||
|
"invalid_ip": "IP Adress ass ong\u00eblteg (MAC Adress konnt net best\u00ebmmt ginn). Korrig\u00e9iert et a prob\u00e9iert et nach eng K\u00e9ier w.e.g.",
|
||||||
|
"invalid_port": "Port muss zw\u00ebscht 1024 a 65535 sinn. Korrig\u00e9iert et a prob\u00e9iert et nach eng K\u00e9ier w.e.g."
|
||||||
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "Apparat",
|
"host": "Apparat",
|
||||||
"name": "Numm",
|
"name": "Numm",
|
||||||
"port": "Port"
|
"port": "Port"
|
||||||
}
|
},
|
||||||
|
"description": "Riicht deng Minecraft Server Instanz a fir d'Iwwerwaachung z'erlaben",
|
||||||
|
"title": "Verbann d\u00e4in Minecraft Server"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Minecraft Server"
|
"title": "Minecraft Server"
|
||||||
|
@@ -4,7 +4,9 @@
|
|||||||
"already_configured": "Host is al geconfigureerd."
|
"already_configured": "Host is al geconfigureerd."
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Kan geen verbinding maken met de server. Controleer de host en de poort en probeer het opnieuw. Zorg er ook voor dat u minimaal Minecraft versie 1.7 op uw server uitvoert."
|
"cannot_connect": "Kan geen verbinding maken met de server. Controleer de host en de poort en probeer het opnieuw. Zorg er ook voor dat u minimaal Minecraft versie 1.7 op uw server uitvoert.",
|
||||||
|
"invalid_ip": "IP-adres is ongeldig (MAC-adres kon niet worden bepaald). Corrigeer het en probeer het opnieuw.",
|
||||||
|
"invalid_port": "Poort moet tussen 1024 en 65535 liggen. Corrigeer dit en probeer het opnieuw."
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
@@ -12,8 +14,11 @@
|
|||||||
"host": "Host",
|
"host": "Host",
|
||||||
"name": "Naam",
|
"name": "Naam",
|
||||||
"port": "Poort"
|
"port": "Poort"
|
||||||
}
|
},
|
||||||
}
|
"description": "Stel uw Minecraft server in om monitoring toe te staan.",
|
||||||
}
|
"title": "Koppel uw Minecraft server"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Minecraft server"
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -159,10 +159,10 @@ def setup(hass, config):
|
|||||||
|
|
||||||
def write_register(service):
|
def write_register(service):
|
||||||
"""Write Modbus registers."""
|
"""Write Modbus registers."""
|
||||||
unit = int(float(service.data.get(ATTR_UNIT)))
|
unit = int(float(service.data[ATTR_UNIT]))
|
||||||
address = int(float(service.data.get(ATTR_ADDRESS)))
|
address = int(float(service.data[ATTR_ADDRESS]))
|
||||||
value = service.data.get(ATTR_VALUE)
|
value = service.data[ATTR_VALUE]
|
||||||
client_name = service.data.get(ATTR_HUB)
|
client_name = service.data[ATTR_HUB]
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
hub_collect[client_name].write_registers(
|
hub_collect[client_name].write_registers(
|
||||||
unit, address, [int(float(i)) for i in value]
|
unit, address, [int(float(i)) for i in value]
|
||||||
@@ -172,10 +172,10 @@ def setup(hass, config):
|
|||||||
|
|
||||||
def write_coil(service):
|
def write_coil(service):
|
||||||
"""Write Modbus coil."""
|
"""Write Modbus coil."""
|
||||||
unit = service.data.get(ATTR_UNIT)
|
unit = service.data[ATTR_UNIT]
|
||||||
address = service.data.get(ATTR_ADDRESS)
|
address = service.data[ATTR_ADDRESS]
|
||||||
state = service.data.get(ATTR_STATE)
|
state = service.data[ATTR_STATE]
|
||||||
client_name = service.data.get(ATTR_HUB)
|
client_name = service.data[ATTR_HUB]
|
||||||
hub_collect[client_name].write_coil(unit, address, state)
|
hub_collect[client_name].write_coil(unit, address, state)
|
||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)
|
||||||
|
@@ -25,8 +25,8 @@ CONF_INPUTS = "inputs"
|
|||||||
CONF_INPUT_TYPE = "input_type"
|
CONF_INPUT_TYPE = "input_type"
|
||||||
CONF_ADDRESS = "address"
|
CONF_ADDRESS = "address"
|
||||||
|
|
||||||
INPUT_TYPE_COIL = "coil"
|
DEFAULT_INPUT_TYPE_COIL = "coil"
|
||||||
INPUT_TYPE_DISCRETE = "discrete_input"
|
DEFAULT_INPUT_TYPE_DISCRETE = "discrete_input"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.All(
|
PLATFORM_SCHEMA = vol.All(
|
||||||
cv.deprecated(CONF_DEPRECATED_COILS, CONF_INPUTS),
|
cv.deprecated(CONF_DEPRECATED_COILS, CONF_INPUTS),
|
||||||
@@ -43,8 +43,10 @@ PLATFORM_SCHEMA = vol.All(
|
|||||||
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
|
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
|
||||||
vol.Optional(CONF_SLAVE): cv.positive_int,
|
vol.Optional(CONF_SLAVE): cv.positive_int,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_INPUT_TYPE, default=INPUT_TYPE_COIL
|
CONF_INPUT_TYPE, default=DEFAULT_INPUT_TYPE_COIL
|
||||||
): vol.In([INPUT_TYPE_COIL, INPUT_TYPE_DISCRETE]),
|
): vol.In(
|
||||||
|
[DEFAULT_INPUT_TYPE_COIL, DEFAULT_INPUT_TYPE_DISCRETE]
|
||||||
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -57,16 +59,16 @@ PLATFORM_SCHEMA = vol.All(
|
|||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the Modbus binary sensors."""
|
"""Set up the Modbus binary sensors."""
|
||||||
sensors = []
|
sensors = []
|
||||||
for entry in config.get(CONF_INPUTS):
|
for entry in config[CONF_INPUTS]:
|
||||||
hub = hass.data[MODBUS_DOMAIN][entry.get(CONF_HUB)]
|
hub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]]
|
||||||
sensors.append(
|
sensors.append(
|
||||||
ModbusBinarySensor(
|
ModbusBinarySensor(
|
||||||
hub,
|
hub,
|
||||||
entry.get(CONF_NAME),
|
entry[CONF_NAME],
|
||||||
entry.get(CONF_SLAVE),
|
entry.get(CONF_SLAVE),
|
||||||
entry.get(CONF_ADDRESS),
|
entry[CONF_ADDRESS],
|
||||||
entry.get(CONF_DEVICE_CLASS),
|
entry.get(CONF_DEVICE_CLASS),
|
||||||
entry.get(CONF_INPUT_TYPE),
|
entry[CONF_INPUT_TYPE],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,7 +112,7 @@ class ModbusBinarySensor(BinarySensorDevice):
|
|||||||
def update(self):
|
def update(self):
|
||||||
"""Update the state of the sensor."""
|
"""Update the state of the sensor."""
|
||||||
try:
|
try:
|
||||||
if self._input_type == INPUT_TYPE_COIL:
|
if self._input_type == DEFAULT_INPUT_TYPE_COIL:
|
||||||
result = self._hub.read_coils(self._slave, self._address, 1)
|
result = self._hub.read_coils(self._slave, self._address, 1)
|
||||||
else:
|
else:
|
||||||
result = self._hub.read_discrete_inputs(self._slave, self._address, 1)
|
result = self._hub.read_discrete_inputs(self._slave, self._address, 1)
|
||||||
|
@@ -27,6 +27,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
CONF_TARGET_TEMP = "target_temp_register"
|
CONF_TARGET_TEMP = "target_temp_register"
|
||||||
CONF_CURRENT_TEMP = "current_temp_register"
|
CONF_CURRENT_TEMP = "current_temp_register"
|
||||||
|
CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type"
|
||||||
CONF_DATA_TYPE = "data_type"
|
CONF_DATA_TYPE = "data_type"
|
||||||
CONF_COUNT = "data_count"
|
CONF_COUNT = "data_count"
|
||||||
CONF_PRECISION = "precision"
|
CONF_PRECISION = "precision"
|
||||||
@@ -42,6 +43,9 @@ CONF_STEP = "temp_step"
|
|||||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
|
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
|
||||||
HVAC_MODES = [HVAC_MODE_AUTO]
|
HVAC_MODES = [HVAC_MODE_AUTO]
|
||||||
|
|
||||||
|
DEFAULT_REGISTER_TYPE_HOLDING = "holding"
|
||||||
|
DEFAULT_REGISTER_TYPE_INPUT = "input"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_CURRENT_TEMP): cv.positive_int,
|
vol.Required(CONF_CURRENT_TEMP): cv.positive_int,
|
||||||
@@ -49,6 +53,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
vol.Required(CONF_SLAVE): cv.positive_int,
|
vol.Required(CONF_SLAVE): cv.positive_int,
|
||||||
vol.Required(CONF_TARGET_TEMP): cv.positive_int,
|
vol.Required(CONF_TARGET_TEMP): cv.positive_int,
|
||||||
vol.Optional(CONF_COUNT, default=2): cv.positive_int,
|
vol.Optional(CONF_COUNT, default=2): cv.positive_int,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CURRENT_TEMP_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING
|
||||||
|
): vol.In([DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]),
|
||||||
vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In(
|
vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In(
|
||||||
[DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]
|
[DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]
|
||||||
),
|
),
|
||||||
@@ -66,20 +73,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the Modbus Thermostat Platform."""
|
"""Set up the Modbus Thermostat Platform."""
|
||||||
name = config.get(CONF_NAME)
|
name = config[CONF_NAME]
|
||||||
modbus_slave = config.get(CONF_SLAVE)
|
modbus_slave = config[CONF_SLAVE]
|
||||||
target_temp_register = config.get(CONF_TARGET_TEMP)
|
target_temp_register = config[CONF_TARGET_TEMP]
|
||||||
current_temp_register = config.get(CONF_CURRENT_TEMP)
|
current_temp_register = config[CONF_CURRENT_TEMP]
|
||||||
data_type = config.get(CONF_DATA_TYPE)
|
current_temp_register_type = config[CONF_CURRENT_TEMP_REGISTER_TYPE]
|
||||||
count = config.get(CONF_COUNT)
|
data_type = config[CONF_DATA_TYPE]
|
||||||
precision = config.get(CONF_PRECISION)
|
count = config[CONF_COUNT]
|
||||||
scale = config.get(CONF_SCALE)
|
precision = config[CONF_PRECISION]
|
||||||
offset = config.get(CONF_OFFSET)
|
scale = config[CONF_SCALE]
|
||||||
unit = config.get(CONF_UNIT)
|
offset = config[CONF_OFFSET]
|
||||||
max_temp = config.get(CONF_MAX_TEMP)
|
unit = config[CONF_UNIT]
|
||||||
min_temp = config.get(CONF_MIN_TEMP)
|
max_temp = config[CONF_MAX_TEMP]
|
||||||
temp_step = config.get(CONF_STEP)
|
min_temp = config[CONF_MIN_TEMP]
|
||||||
hub_name = config.get(CONF_HUB)
|
temp_step = config[CONF_STEP]
|
||||||
|
hub_name = config[CONF_HUB]
|
||||||
hub = hass.data[MODBUS_DOMAIN][hub_name]
|
hub = hass.data[MODBUS_DOMAIN][hub_name]
|
||||||
|
|
||||||
add_entities(
|
add_entities(
|
||||||
@@ -90,6 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
modbus_slave,
|
modbus_slave,
|
||||||
target_temp_register,
|
target_temp_register,
|
||||||
current_temp_register,
|
current_temp_register,
|
||||||
|
current_temp_register_type,
|
||||||
data_type,
|
data_type,
|
||||||
count,
|
count,
|
||||||
precision,
|
precision,
|
||||||
@@ -115,6 +124,7 @@ class ModbusThermostat(ClimateDevice):
|
|||||||
modbus_slave,
|
modbus_slave,
|
||||||
target_temp_register,
|
target_temp_register,
|
||||||
current_temp_register,
|
current_temp_register,
|
||||||
|
current_temp_register_type,
|
||||||
data_type,
|
data_type,
|
||||||
count,
|
count,
|
||||||
precision,
|
precision,
|
||||||
@@ -131,6 +141,7 @@ class ModbusThermostat(ClimateDevice):
|
|||||||
self._slave = modbus_slave
|
self._slave = modbus_slave
|
||||||
self._target_temperature_register = target_temp_register
|
self._target_temperature_register = target_temp_register
|
||||||
self._current_temperature_register = current_temp_register
|
self._current_temperature_register = current_temp_register
|
||||||
|
self._current_temperature_register_type = current_temp_register_type
|
||||||
self._target_temperature = None
|
self._target_temperature = None
|
||||||
self._current_temperature = None
|
self._current_temperature = None
|
||||||
self._data_type = data_type
|
self._data_type = data_type
|
||||||
@@ -161,10 +172,10 @@ class ModbusThermostat(ClimateDevice):
|
|||||||
def update(self):
|
def update(self):
|
||||||
"""Update Target & Current Temperature."""
|
"""Update Target & Current Temperature."""
|
||||||
self._target_temperature = self._read_register(
|
self._target_temperature = self._read_register(
|
||||||
self._target_temperature_register
|
DEFAULT_REGISTER_TYPE_HOLDING, self._target_temperature_register
|
||||||
)
|
)
|
||||||
self._current_temperature = self._read_register(
|
self._current_temperature = self._read_register(
|
||||||
self._current_temperature_register
|
self._current_temperature_register_type, self._current_temperature_register
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -228,9 +239,14 @@ class ModbusThermostat(ClimateDevice):
|
|||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return self._available
|
return self._available
|
||||||
|
|
||||||
def _read_register(self, register) -> Optional[float]:
|
def _read_register(self, register_type, register) -> Optional[float]:
|
||||||
"""Read holding register using the Modbus hub slave."""
|
"""Read register using the Modbus hub slave."""
|
||||||
try:
|
try:
|
||||||
|
if register_type == DEFAULT_REGISTER_TYPE_INPUT:
|
||||||
|
result = self._hub.read_input_registers(
|
||||||
|
self._slave, register, self._count
|
||||||
|
)
|
||||||
|
else:
|
||||||
result = self._hub.read_holding_registers(
|
result = self._hub.read_holding_registers(
|
||||||
self._slave, register, self._count
|
self._slave, register, self._count
|
||||||
)
|
)
|
||||||
|
@@ -37,8 +37,8 @@ DATA_TYPE_FLOAT = "float"
|
|||||||
DATA_TYPE_INT = "int"
|
DATA_TYPE_INT = "int"
|
||||||
DATA_TYPE_UINT = "uint"
|
DATA_TYPE_UINT = "uint"
|
||||||
|
|
||||||
REGISTER_TYPE_HOLDING = "holding"
|
DEFAULT_REGISTER_TYPE_HOLDING = "holding"
|
||||||
REGISTER_TYPE_INPUT = "input"
|
DEFAULT_REGISTER_TYPE_INPUT = "input"
|
||||||
|
|
||||||
|
|
||||||
def number(value: Any) -> Union[int, float]:
|
def number(value: Any) -> Union[int, float]:
|
||||||
@@ -74,9 +74,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
|
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
|
||||||
vol.Optional(CONF_OFFSET, default=0): number,
|
vol.Optional(CONF_OFFSET, default=0): number,
|
||||||
vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
|
vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
|
||||||
vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): vol.In(
|
vol.Optional(
|
||||||
[REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]
|
CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING
|
||||||
),
|
): vol.In([DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]),
|
||||||
vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean,
|
vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean,
|
||||||
vol.Optional(CONF_SCALE, default=1): number,
|
vol.Optional(CONF_SCALE, default=1): number,
|
||||||
vol.Optional(CONF_SLAVE): cv.positive_int,
|
vol.Optional(CONF_SLAVE): cv.positive_int,
|
||||||
@@ -95,17 +95,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
data_types[DATA_TYPE_UINT] = {1: "H", 2: "I", 4: "Q"}
|
data_types[DATA_TYPE_UINT] = {1: "H", 2: "I", 4: "Q"}
|
||||||
data_types[DATA_TYPE_FLOAT] = {1: "e", 2: "f", 4: "d"}
|
data_types[DATA_TYPE_FLOAT] = {1: "e", 2: "f", 4: "d"}
|
||||||
|
|
||||||
for register in config.get(CONF_REGISTERS):
|
for register in config[CONF_REGISTERS]:
|
||||||
structure = ">i"
|
structure = ">i"
|
||||||
if register.get(CONF_DATA_TYPE) != DATA_TYPE_CUSTOM:
|
if register[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM:
|
||||||
try:
|
try:
|
||||||
structure = ">{}".format(
|
structure = ">{}".format(
|
||||||
data_types[register.get(CONF_DATA_TYPE)][register.get(CONF_COUNT)]
|
data_types[register[CONF_DATA_TYPE]][register[CONF_COUNT]]
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Unable to detect data type for %s sensor, try a custom type",
|
"Unable to detect data type for %s sensor, try a custom type",
|
||||||
register.get(CONF_NAME),
|
register[CONF_NAME],
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
@@ -114,35 +114,33 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
try:
|
try:
|
||||||
size = struct.calcsize(structure)
|
size = struct.calcsize(structure)
|
||||||
except struct.error as err:
|
except struct.error as err:
|
||||||
_LOGGER.error(
|
_LOGGER.error("Error in sensor %s structure: %s", register[CONF_NAME], err)
|
||||||
"Error in sensor %s structure: %s", register.get(CONF_NAME), err
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if register.get(CONF_COUNT) * 2 != size:
|
if register[CONF_COUNT] * 2 != size:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Structure size (%d bytes) mismatch registers count (%d words)",
|
"Structure size (%d bytes) mismatch registers count (%d words)",
|
||||||
size,
|
size,
|
||||||
register.get(CONF_COUNT),
|
register[CONF_COUNT],
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
hub_name = register.get(CONF_HUB)
|
hub_name = register[CONF_HUB]
|
||||||
hub = hass.data[MODBUS_DOMAIN][hub_name]
|
hub = hass.data[MODBUS_DOMAIN][hub_name]
|
||||||
sensors.append(
|
sensors.append(
|
||||||
ModbusRegisterSensor(
|
ModbusRegisterSensor(
|
||||||
hub,
|
hub,
|
||||||
register.get(CONF_NAME),
|
register[CONF_NAME],
|
||||||
register.get(CONF_SLAVE),
|
register.get(CONF_SLAVE),
|
||||||
register.get(CONF_REGISTER),
|
register[CONF_REGISTER],
|
||||||
register.get(CONF_REGISTER_TYPE),
|
register[CONF_REGISTER_TYPE],
|
||||||
register.get(CONF_UNIT_OF_MEASUREMENT),
|
register.get(CONF_UNIT_OF_MEASUREMENT),
|
||||||
register.get(CONF_COUNT),
|
register[CONF_COUNT],
|
||||||
register.get(CONF_REVERSE_ORDER),
|
register[CONF_REVERSE_ORDER],
|
||||||
register.get(CONF_SCALE),
|
register[CONF_SCALE],
|
||||||
register.get(CONF_OFFSET),
|
register[CONF_OFFSET],
|
||||||
structure,
|
structure,
|
||||||
register.get(CONF_PRECISION),
|
register[CONF_PRECISION],
|
||||||
register.get(CONF_DEVICE_CLASS),
|
register.get(CONF_DEVICE_CLASS),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -223,7 +221,7 @@ class ModbusRegisterSensor(RestoreEntity):
|
|||||||
def update(self):
|
def update(self):
|
||||||
"""Update the state of the sensor."""
|
"""Update the state of the sensor."""
|
||||||
try:
|
try:
|
||||||
if self._register_type == REGISTER_TYPE_INPUT:
|
if self._register_type == DEFAULT_REGISTER_TYPE_INPUT:
|
||||||
result = self._hub.read_input_registers(
|
result = self._hub.read_input_registers(
|
||||||
self._slave, self._register, self._count
|
self._slave, self._register, self._count
|
||||||
)
|
)
|
||||||
|
@@ -32,8 +32,8 @@ CONF_STATE_ON = "state_on"
|
|||||||
CONF_VERIFY_REGISTER = "verify_register"
|
CONF_VERIFY_REGISTER = "verify_register"
|
||||||
CONF_VERIFY_STATE = "verify_state"
|
CONF_VERIFY_STATE = "verify_state"
|
||||||
|
|
||||||
REGISTER_TYPE_HOLDING = "holding"
|
DEFAULT_REGISTER_TYPE_HOLDING = "holding"
|
||||||
REGISTER_TYPE_INPUT = "input"
|
DEFAULT_REGISTER_TYPE_INPUT = "input"
|
||||||
|
|
||||||
REGISTERS_SCHEMA = vol.Schema(
|
REGISTERS_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@@ -42,8 +42,8 @@ REGISTERS_SCHEMA = vol.Schema(
|
|||||||
vol.Required(CONF_NAME): cv.string,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
vol.Required(CONF_REGISTER): cv.positive_int,
|
vol.Required(CONF_REGISTER): cv.positive_int,
|
||||||
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
|
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
|
||||||
vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): vol.In(
|
vol.Optional(CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING): vol.In(
|
||||||
[REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]
|
[DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]
|
||||||
),
|
),
|
||||||
vol.Optional(CONF_SLAVE): cv.positive_int,
|
vol.Optional(CONF_SLAVE): cv.positive_int,
|
||||||
vol.Optional(CONF_STATE_OFF): cv.positive_int,
|
vol.Optional(CONF_STATE_OFF): cv.positive_int,
|
||||||
@@ -77,30 +77,30 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
"""Read configuration and create Modbus devices."""
|
"""Read configuration and create Modbus devices."""
|
||||||
switches = []
|
switches = []
|
||||||
if CONF_COILS in config:
|
if CONF_COILS in config:
|
||||||
for coil in config.get(CONF_COILS):
|
for coil in config[CONF_COILS]:
|
||||||
hub_name = coil.get(CONF_HUB)
|
hub_name = coil[CONF_HUB]
|
||||||
hub = hass.data[MODBUS_DOMAIN][hub_name]
|
hub = hass.data[MODBUS_DOMAIN][hub_name]
|
||||||
switches.append(
|
switches.append(
|
||||||
ModbusCoilSwitch(
|
ModbusCoilSwitch(
|
||||||
hub, coil.get(CONF_NAME), coil.get(CONF_SLAVE), coil.get(CONF_COIL)
|
hub, coil[CONF_NAME], coil[CONF_SLAVE], coil[CONF_COIL]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if CONF_REGISTERS in config:
|
if CONF_REGISTERS in config:
|
||||||
for register in config.get(CONF_REGISTERS):
|
for register in config[CONF_REGISTERS]:
|
||||||
hub_name = register.get(CONF_HUB)
|
hub_name = register[CONF_HUB]
|
||||||
hub = hass.data[MODBUS_DOMAIN][hub_name]
|
hub = hass.data[MODBUS_DOMAIN][hub_name]
|
||||||
|
|
||||||
switches.append(
|
switches.append(
|
||||||
ModbusRegisterSwitch(
|
ModbusRegisterSwitch(
|
||||||
hub,
|
hub,
|
||||||
register.get(CONF_NAME),
|
register[CONF_NAME],
|
||||||
register.get(CONF_SLAVE),
|
register.get(CONF_SLAVE),
|
||||||
register.get(CONF_REGISTER),
|
register[CONF_REGISTER],
|
||||||
register.get(CONF_COMMAND_ON),
|
register[CONF_COMMAND_ON],
|
||||||
register.get(CONF_COMMAND_OFF),
|
register[CONF_COMMAND_OFF],
|
||||||
register.get(CONF_VERIFY_STATE),
|
register[CONF_VERIFY_STATE],
|
||||||
register.get(CONF_VERIFY_REGISTER),
|
register.get(CONF_VERIFY_REGISTER),
|
||||||
register.get(CONF_REGISTER_TYPE),
|
register[CONF_REGISTER_TYPE],
|
||||||
register.get(CONF_STATE_ON),
|
register.get(CONF_STATE_ON),
|
||||||
register.get(CONF_STATE_OFF),
|
register.get(CONF_STATE_OFF),
|
||||||
)
|
)
|
||||||
@@ -242,7 +242,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
|
|||||||
"""Set switch on."""
|
"""Set switch on."""
|
||||||
|
|
||||||
# Only holding register is writable
|
# Only holding register is writable
|
||||||
if self._register_type == REGISTER_TYPE_HOLDING:
|
if self._register_type == DEFAULT_REGISTER_TYPE_HOLDING:
|
||||||
self._write_register(self._command_on)
|
self._write_register(self._command_on)
|
||||||
if not self._verify_state:
|
if not self._verify_state:
|
||||||
self._is_on = True
|
self._is_on = True
|
||||||
@@ -251,7 +251,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
|
|||||||
"""Set switch off."""
|
"""Set switch off."""
|
||||||
|
|
||||||
# Only holding register is writable
|
# Only holding register is writable
|
||||||
if self._register_type == REGISTER_TYPE_HOLDING:
|
if self._register_type == DEFAULT_REGISTER_TYPE_HOLDING:
|
||||||
self._write_register(self._command_off)
|
self._write_register(self._command_off)
|
||||||
if not self._verify_state:
|
if not self._verify_state:
|
||||||
self._is_on = False
|
self._is_on = False
|
||||||
@@ -282,7 +282,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
|
|||||||
|
|
||||||
def _read_register(self) -> Optional[int]:
|
def _read_register(self) -> Optional[int]:
|
||||||
try:
|
try:
|
||||||
if self._register_type == REGISTER_TYPE_INPUT:
|
if self._register_type == DEFAULT_REGISTER_TYPE_INPUT:
|
||||||
result = self._hub.read_input_registers(self._slave, self._register, 1)
|
result = self._hub.read_input_registers(self._slave, self._register, 1)
|
||||||
else:
|
else:
|
||||||
result = self._hub.read_holding_registers(
|
result = self._hub.read_holding_registers(
|
||||||
|
@@ -27,5 +27,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "MQTT"
|
"title": "MQTT"
|
||||||
|
},
|
||||||
|
"device_automation": {
|
||||||
|
"trigger_subtype": {
|
||||||
|
"button_1": "First button",
|
||||||
|
"button_2": "Second button",
|
||||||
|
"button_3": "Third button",
|
||||||
|
"button_4": "Fourth button",
|
||||||
|
"button_5": "Fifth button",
|
||||||
|
"button_6": "Sixth button",
|
||||||
|
"turn_off": "Turn off",
|
||||||
|
"turn_on": "Turn on"
|
||||||
|
},
|
||||||
|
"trigger_type": {
|
||||||
|
"button_double_press": "\"{subtype}\" double clicked",
|
||||||
|
"button_long_press": "\"{subtype}\" continuously pressed",
|
||||||
|
"button_long_release": "\"{subtype}\" released after long press",
|
||||||
|
"button_quadruple_press": "\"{subtype}\" quadruple clicked",
|
||||||
|
"button_quintuple_press": "\"{subtype}\" quintuple clicked",
|
||||||
|
"button_short_press": "\"{subtype}\" pressed",
|
||||||
|
"button_short_release": "\"{subtype}\" released",
|
||||||
|
"button_triple_press": "\"{subtype}\" triple clicked"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1194,6 +1194,34 @@ class MqttDiscoveryUpdate(Entity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def device_info_from_config(config):
|
||||||
|
"""Return a device description for device registry."""
|
||||||
|
if not config:
|
||||||
|
return None
|
||||||
|
|
||||||
|
info = {
|
||||||
|
"identifiers": {(DOMAIN, id_) for id_ in config[CONF_IDENTIFIERS]},
|
||||||
|
"connections": {tuple(x) for x in config[CONF_CONNECTIONS]},
|
||||||
|
}
|
||||||
|
|
||||||
|
if CONF_MANUFACTURER in config:
|
||||||
|
info["manufacturer"] = config[CONF_MANUFACTURER]
|
||||||
|
|
||||||
|
if CONF_MODEL in config:
|
||||||
|
info["model"] = config[CONF_MODEL]
|
||||||
|
|
||||||
|
if CONF_NAME in config:
|
||||||
|
info["name"] = config[CONF_NAME]
|
||||||
|
|
||||||
|
if CONF_SW_VERSION in config:
|
||||||
|
info["sw_version"] = config[CONF_SW_VERSION]
|
||||||
|
|
||||||
|
if CONF_VIA_DEVICE in config:
|
||||||
|
info["via_device"] = (DOMAIN, config[CONF_VIA_DEVICE])
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
class MqttEntityDeviceInfo(Entity):
|
class MqttEntityDeviceInfo(Entity):
|
||||||
"""Mixin used for mqtt platforms that support the device registry."""
|
"""Mixin used for mqtt platforms that support the device registry."""
|
||||||
|
|
||||||
@@ -1216,32 +1244,7 @@ class MqttEntityDeviceInfo(Entity):
|
|||||||
@property
|
@property
|
||||||
def device_info(self):
|
def device_info(self):
|
||||||
"""Return a device description for device registry."""
|
"""Return a device description for device registry."""
|
||||||
if not self._device_config:
|
return device_info_from_config(self._device_config)
|
||||||
return None
|
|
||||||
|
|
||||||
info = {
|
|
||||||
"identifiers": {
|
|
||||||
(DOMAIN, id_) for id_ in self._device_config[CONF_IDENTIFIERS]
|
|
||||||
},
|
|
||||||
"connections": {tuple(x) for x in self._device_config[CONF_CONNECTIONS]},
|
|
||||||
}
|
|
||||||
|
|
||||||
if CONF_MANUFACTURER in self._device_config:
|
|
||||||
info["manufacturer"] = self._device_config[CONF_MANUFACTURER]
|
|
||||||
|
|
||||||
if CONF_MODEL in self._device_config:
|
|
||||||
info["model"] = self._device_config[CONF_MODEL]
|
|
||||||
|
|
||||||
if CONF_NAME in self._device_config:
|
|
||||||
info["name"] = self._device_config[CONF_NAME]
|
|
||||||
|
|
||||||
if CONF_SW_VERSION in self._device_config:
|
|
||||||
info["sw_version"] = self._device_config[CONF_SW_VERSION]
|
|
||||||
|
|
||||||
if CONF_VIA_DEVICE in self._device_config:
|
|
||||||
info["via_device"] = (DOMAIN, self._device_config[CONF_VIA_DEVICE])
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
ABBREVIATIONS = {
|
ABBREVIATIONS = {
|
||||||
"act_t": "action_topic",
|
"act_t": "action_topic",
|
||||||
"act_tpl": "action_template",
|
"act_tpl": "action_template",
|
||||||
|
"atype": "automation_type",
|
||||||
"aux_cmd_t": "aux_command_topic",
|
"aux_cmd_t": "aux_command_topic",
|
||||||
"aux_stat_tpl": "aux_state_template",
|
"aux_stat_tpl": "aux_state_template",
|
||||||
"aux_stat_t": "aux_state_topic",
|
"aux_stat_t": "aux_state_topic",
|
||||||
@@ -80,6 +81,7 @@ ABBREVIATIONS = {
|
|||||||
"osc_cmd_t": "oscillation_command_topic",
|
"osc_cmd_t": "oscillation_command_topic",
|
||||||
"osc_stat_t": "oscillation_state_topic",
|
"osc_stat_t": "oscillation_state_topic",
|
||||||
"osc_val_tpl": "oscillation_value_template",
|
"osc_val_tpl": "oscillation_value_template",
|
||||||
|
"pl": "payload",
|
||||||
"pl_arm_away": "payload_arm_away",
|
"pl_arm_away": "payload_arm_away",
|
||||||
"pl_arm_home": "payload_arm_home",
|
"pl_arm_home": "payload_arm_home",
|
||||||
"pl_arm_nite": "payload_arm_night",
|
"pl_arm_nite": "payload_arm_night",
|
||||||
@@ -142,6 +144,7 @@ ABBREVIATIONS = {
|
|||||||
"stat_t": "state_topic",
|
"stat_t": "state_topic",
|
||||||
"stat_tpl": "state_template",
|
"stat_tpl": "state_template",
|
||||||
"stat_val_tpl": "state_value_template",
|
"stat_val_tpl": "state_value_template",
|
||||||
|
"stype": "subtype",
|
||||||
"sup_feat": "supported_features",
|
"sup_feat": "supported_features",
|
||||||
"swing_mode_cmd_t": "swing_mode_command_topic",
|
"swing_mode_cmd_t": "swing_mode_command_topic",
|
||||||
"swing_mode_stat_tpl": "swing_mode_state_template",
|
"swing_mode_stat_tpl": "swing_mode_state_template",
|
||||||
|
@@ -178,14 +178,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||||||
|
|
||||||
async def async_discover(discovery_payload):
|
async def async_discover(discovery_payload):
|
||||||
"""Discover and add an MQTT cover."""
|
"""Discover and add an MQTT cover."""
|
||||||
try:
|
|
||||||
discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH)
|
discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH)
|
||||||
|
try:
|
||||||
config = PLATFORM_SCHEMA(discovery_payload)
|
config = PLATFORM_SCHEMA(discovery_payload)
|
||||||
await _async_setup_entity(
|
await _async_setup_entity(
|
||||||
config, async_add_entities, config_entry, discovery_hash
|
config, async_add_entities, config_entry, discovery_hash
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
if discovery_hash:
|
|
||||||
clear_discovery_hash(hass, discovery_hash)
|
clear_discovery_hash(hass, discovery_hash)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
44
homeassistant/components/mqtt/device_automation.py
Normal file
44
homeassistant/components/mqtt/device_automation.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
"""Provides device automations for MQTT."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import mqtt
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
|
from . import ATTR_DISCOVERY_HASH, device_trigger
|
||||||
|
from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
AUTOMATION_TYPE_TRIGGER = "trigger"
|
||||||
|
AUTOMATION_TYPES = [AUTOMATION_TYPE_TRIGGER]
|
||||||
|
AUTOMATION_TYPES_SCHEMA = vol.In(AUTOMATION_TYPES)
|
||||||
|
CONF_AUTOMATION_TYPE = "automation_type"
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
|
||||||
|
{vol.Required(CONF_AUTOMATION_TYPE): AUTOMATION_TYPES_SCHEMA},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry):
|
||||||
|
"""Set up MQTT device automation dynamically through MQTT discovery."""
|
||||||
|
|
||||||
|
async def async_discover(discovery_payload):
|
||||||
|
"""Discover and add an MQTT device automation."""
|
||||||
|
discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH)
|
||||||
|
try:
|
||||||
|
config = PLATFORM_SCHEMA(discovery_payload)
|
||||||
|
if config[CONF_AUTOMATION_TYPE] == AUTOMATION_TYPE_TRIGGER:
|
||||||
|
await device_trigger.async_setup_trigger(
|
||||||
|
hass, config, config_entry, discovery_hash
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
if discovery_hash:
|
||||||
|
clear_discovery_hash(hass, discovery_hash)
|
||||||
|
raise
|
||||||
|
|
||||||
|
async_dispatcher_connect(
|
||||||
|
hass, MQTT_DISCOVERY_NEW.format("device_automation", "mqtt"), async_discover
|
||||||
|
)
|
273
homeassistant/components/mqtt/device_trigger.py
Normal file
273
homeassistant/components/mqtt/device_trigger.py
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
"""Provides device automations for MQTT."""
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import attr
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import mqtt
|
||||||
|
from homeassistant.components.automation import AutomationActionType
|
||||||
|
import homeassistant.components.automation.mqtt as automation_mqtt
|
||||||
|
from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA
|
||||||
|
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
|
||||||
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
ATTR_DISCOVERY_HASH,
|
||||||
|
CONF_CONNECTIONS,
|
||||||
|
CONF_DEVICE,
|
||||||
|
CONF_IDENTIFIERS,
|
||||||
|
CONF_PAYLOAD,
|
||||||
|
CONF_QOS,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_AUTOMATION_TYPE = "automation_type"
|
||||||
|
CONF_DISCOVERY_ID = "discovery_id"
|
||||||
|
CONF_SUBTYPE = "subtype"
|
||||||
|
CONF_TOPIC = "topic"
|
||||||
|
DEFAULT_ENCODING = "utf-8"
|
||||||
|
DEVICE = "device"
|
||||||
|
|
||||||
|
MQTT_TRIGGER_BASE = {
|
||||||
|
# Trigger when MQTT message is received
|
||||||
|
CONF_PLATFORM: DEVICE,
|
||||||
|
CONF_DOMAIN: DOMAIN,
|
||||||
|
}
|
||||||
|
|
||||||
|
TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_PLATFORM): DEVICE,
|
||||||
|
vol.Required(CONF_DOMAIN): DOMAIN,
|
||||||
|
vol.Required(CONF_DEVICE_ID): str,
|
||||||
|
vol.Required(CONF_DISCOVERY_ID): str,
|
||||||
|
vol.Required(CONF_TYPE): cv.string,
|
||||||
|
vol.Required(CONF_SUBTYPE): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_AUTOMATION_TYPE): str,
|
||||||
|
vol.Required(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||||
|
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
|
||||||
|
vol.Optional(CONF_PAYLOAD, default=None): vol.Any(None, cv.string),
|
||||||
|
vol.Required(CONF_TYPE): cv.string,
|
||||||
|
vol.Required(CONF_SUBTYPE): cv.string,
|
||||||
|
},
|
||||||
|
mqtt.validate_device_has_at_least_one_identifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
DEVICE_TRIGGERS = "mqtt_device_triggers"
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True)
|
||||||
|
class TriggerInstance:
|
||||||
|
"""Attached trigger settings."""
|
||||||
|
|
||||||
|
action = attr.ib(type=AutomationActionType)
|
||||||
|
automation_info = attr.ib(type=dict)
|
||||||
|
trigger = attr.ib(type="Trigger")
|
||||||
|
remove = attr.ib(type=CALLBACK_TYPE, default=None)
|
||||||
|
|
||||||
|
async def async_attach_trigger(self):
|
||||||
|
"""Attach MQTT trigger."""
|
||||||
|
mqtt_config = {
|
||||||
|
automation_mqtt.CONF_TOPIC: self.trigger.topic,
|
||||||
|
automation_mqtt.CONF_ENCODING: DEFAULT_ENCODING,
|
||||||
|
automation_mqtt.CONF_QOS: self.trigger.qos,
|
||||||
|
}
|
||||||
|
if self.trigger.payload:
|
||||||
|
mqtt_config[CONF_PAYLOAD] = self.trigger.payload
|
||||||
|
|
||||||
|
if self.remove:
|
||||||
|
self.remove()
|
||||||
|
self.remove = await automation_mqtt.async_attach_trigger(
|
||||||
|
self.trigger.hass, mqtt_config, self.action, self.automation_info,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(slots=True)
|
||||||
|
class Trigger:
|
||||||
|
"""Device trigger settings."""
|
||||||
|
|
||||||
|
device_id = attr.ib(type=str)
|
||||||
|
hass = attr.ib(type=HomeAssistantType)
|
||||||
|
payload = attr.ib(type=str)
|
||||||
|
qos = attr.ib(type=int)
|
||||||
|
subtype = attr.ib(type=str)
|
||||||
|
topic = attr.ib(type=str)
|
||||||
|
type = attr.ib(type=str)
|
||||||
|
trigger_instances = attr.ib(type=[TriggerInstance], default=attr.Factory(list))
|
||||||
|
|
||||||
|
async def add_trigger(self, action, automation_info):
|
||||||
|
"""Add MQTT trigger."""
|
||||||
|
instance = TriggerInstance(action, automation_info, self)
|
||||||
|
self.trigger_instances.append(instance)
|
||||||
|
|
||||||
|
if self.topic is not None:
|
||||||
|
# If we know about the trigger, subscribe to MQTT topic
|
||||||
|
await instance.async_attach_trigger()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_remove() -> None:
|
||||||
|
"""Remove trigger."""
|
||||||
|
if instance not in self.trigger_instances:
|
||||||
|
raise HomeAssistantError("Can't remove trigger twice")
|
||||||
|
|
||||||
|
if instance.remove:
|
||||||
|
instance.remove()
|
||||||
|
self.trigger_instances.remove(instance)
|
||||||
|
|
||||||
|
return async_remove
|
||||||
|
|
||||||
|
async def update_trigger(self, config):
|
||||||
|
"""Update MQTT device trigger."""
|
||||||
|
self.type = config[CONF_TYPE]
|
||||||
|
self.subtype = config[CONF_SUBTYPE]
|
||||||
|
self.topic = config[CONF_TOPIC]
|
||||||
|
self.payload = config[CONF_PAYLOAD]
|
||||||
|
self.qos = config[CONF_QOS]
|
||||||
|
|
||||||
|
# Unsubscribe+subscribe if this trigger is in use
|
||||||
|
for trig in self.trigger_instances:
|
||||||
|
await trig.async_attach_trigger()
|
||||||
|
|
||||||
|
def detach_trigger(self):
|
||||||
|
"""Remove MQTT device trigger."""
|
||||||
|
# Mark trigger as unknown
|
||||||
|
|
||||||
|
self.topic = None
|
||||||
|
# Unsubscribe if this trigger is in use
|
||||||
|
for trig in self.trigger_instances:
|
||||||
|
if trig.remove:
|
||||||
|
trig.remove()
|
||||||
|
trig.remove = None
|
||||||
|
|
||||||
|
|
||||||
|
async def _update_device(hass, config_entry, config):
|
||||||
|
"""Update device registry."""
|
||||||
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
config_entry_id = config_entry.entry_id
|
||||||
|
device_info = mqtt.device_info_from_config(config[CONF_DEVICE])
|
||||||
|
|
||||||
|
if config_entry_id is not None and device_info is not None:
|
||||||
|
device_info["config_entry_id"] = config_entry_id
|
||||||
|
device_registry.async_get_or_create(**device_info)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_trigger(hass, config, config_entry, discovery_hash):
|
||||||
|
"""Set up the MQTT device trigger."""
|
||||||
|
config = TRIGGER_DISCOVERY_SCHEMA(config)
|
||||||
|
discovery_id = discovery_hash[1]
|
||||||
|
remove_signal = None
|
||||||
|
|
||||||
|
async def discovery_update(payload):
|
||||||
|
"""Handle discovery update."""
|
||||||
|
_LOGGER.info(
|
||||||
|
"Got update for trigger with hash: %s '%s'", discovery_hash, payload
|
||||||
|
)
|
||||||
|
if not payload:
|
||||||
|
# Empty payload: Remove trigger
|
||||||
|
_LOGGER.info("Removing trigger: %s", discovery_hash)
|
||||||
|
if discovery_id in hass.data[DEVICE_TRIGGERS]:
|
||||||
|
device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id]
|
||||||
|
device_trigger.detach_trigger()
|
||||||
|
clear_discovery_hash(hass, discovery_hash)
|
||||||
|
remove_signal()
|
||||||
|
else:
|
||||||
|
# Non-empty payload: Update trigger
|
||||||
|
_LOGGER.info("Updating trigger: %s", discovery_hash)
|
||||||
|
payload.pop(ATTR_DISCOVERY_HASH)
|
||||||
|
config = TRIGGER_DISCOVERY_SCHEMA(payload)
|
||||||
|
await _update_device(hass, config_entry, config)
|
||||||
|
device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id]
|
||||||
|
await device_trigger.update_trigger(config)
|
||||||
|
|
||||||
|
remove_signal = async_dispatcher_connect(
|
||||||
|
hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), discovery_update
|
||||||
|
)
|
||||||
|
|
||||||
|
await _update_device(hass, config_entry, config)
|
||||||
|
|
||||||
|
device_registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
device = device_registry.async_get_device(
|
||||||
|
{(DOMAIN, id_) for id_ in config[CONF_DEVICE][CONF_IDENTIFIERS]},
|
||||||
|
{tuple(x) for x in config[CONF_DEVICE][CONF_CONNECTIONS]},
|
||||||
|
)
|
||||||
|
|
||||||
|
if device is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if DEVICE_TRIGGERS not in hass.data:
|
||||||
|
hass.data[DEVICE_TRIGGERS] = {}
|
||||||
|
if discovery_id not in hass.data[DEVICE_TRIGGERS]:
|
||||||
|
hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger(
|
||||||
|
hass=hass,
|
||||||
|
device_id=device.id,
|
||||||
|
type=config[CONF_TYPE],
|
||||||
|
subtype=config[CONF_SUBTYPE],
|
||||||
|
topic=config[CONF_TOPIC],
|
||||||
|
payload=config[CONF_PAYLOAD],
|
||||||
|
qos=config[CONF_QOS],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger(config)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]:
|
||||||
|
"""List device triggers for MQTT devices."""
|
||||||
|
triggers = []
|
||||||
|
|
||||||
|
if DEVICE_TRIGGERS not in hass.data:
|
||||||
|
return triggers
|
||||||
|
|
||||||
|
for discovery_id, trig in hass.data[DEVICE_TRIGGERS].items():
|
||||||
|
if trig.device_id != device_id or trig.topic is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
trigger = {
|
||||||
|
**MQTT_TRIGGER_BASE,
|
||||||
|
"device_id": device_id,
|
||||||
|
"type": trig.type,
|
||||||
|
"subtype": trig.subtype,
|
||||||
|
"discovery_id": discovery_id,
|
||||||
|
}
|
||||||
|
triggers.append(trigger)
|
||||||
|
|
||||||
|
return triggers
|
||||||
|
|
||||||
|
|
||||||
|
async def async_attach_trigger(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: ConfigType,
|
||||||
|
action: AutomationActionType,
|
||||||
|
automation_info: dict,
|
||||||
|
) -> CALLBACK_TYPE:
|
||||||
|
"""Attach a trigger."""
|
||||||
|
if DEVICE_TRIGGERS not in hass.data:
|
||||||
|
hass.data[DEVICE_TRIGGERS] = {}
|
||||||
|
config = TRIGGER_SCHEMA(config)
|
||||||
|
device_id = config[CONF_DEVICE_ID]
|
||||||
|
discovery_id = config[CONF_DISCOVERY_ID]
|
||||||
|
|
||||||
|
if discovery_id not in hass.data[DEVICE_TRIGGERS]:
|
||||||
|
hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger(
|
||||||
|
hass=hass,
|
||||||
|
device_id=device_id,
|
||||||
|
type=config[CONF_TYPE],
|
||||||
|
subtype=config[CONF_SUBTYPE],
|
||||||
|
topic=None,
|
||||||
|
payload=None,
|
||||||
|
qos=None,
|
||||||
|
)
|
||||||
|
return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger(
|
||||||
|
action, automation_info
|
||||||
|
)
|
@@ -26,6 +26,7 @@ SUPPORTED_COMPONENTS = [
|
|||||||
"camera",
|
"camera",
|
||||||
"climate",
|
"climate",
|
||||||
"cover",
|
"cover",
|
||||||
|
"device_automation",
|
||||||
"fan",
|
"fan",
|
||||||
"light",
|
"light",
|
||||||
"lock",
|
"lock",
|
||||||
@@ -40,6 +41,7 @@ CONFIG_ENTRY_COMPONENTS = [
|
|||||||
"camera",
|
"camera",
|
||||||
"climate",
|
"climate",
|
||||||
"cover",
|
"cover",
|
||||||
|
"device_automation",
|
||||||
"fan",
|
"fan",
|
||||||
"light",
|
"light",
|
||||||
"lock",
|
"lock",
|
||||||
@@ -197,6 +199,12 @@ async def async_start(
|
|||||||
config_entries_key = "{}.{}".format(component, "mqtt")
|
config_entries_key = "{}.{}".format(component, "mqtt")
|
||||||
async with hass.data[DATA_CONFIG_ENTRY_LOCK]:
|
async with hass.data[DATA_CONFIG_ENTRY_LOCK]:
|
||||||
if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]:
|
if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]:
|
||||||
|
if component == "device_automation":
|
||||||
|
# Local import to avoid circular dependencies
|
||||||
|
from . import device_automation
|
||||||
|
|
||||||
|
await device_automation.async_setup_entry(hass, config_entry)
|
||||||
|
else:
|
||||||
await hass.config_entries.async_forward_entry_setup(
|
await hass.config_entries.async_forward_entry_setup(
|
||||||
config_entry, component
|
config_entry, component
|
||||||
)
|
)
|
||||||
|
@@ -27,5 +27,27 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Unable to connect to the broker."
|
"cannot_connect": "Unable to connect to the broker."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"device_automation": {
|
||||||
|
"trigger_type": {
|
||||||
|
"button_short_press": "\"{subtype}\" pressed",
|
||||||
|
"button_short_release": "\"{subtype}\" released",
|
||||||
|
"button_long_press": "\"{subtype}\" continuously pressed",
|
||||||
|
"button_long_release": "\"{subtype}\" released after long press",
|
||||||
|
"button_double_press": "\"{subtype}\" double clicked",
|
||||||
|
"button_triple_press": "\"{subtype}\" triple clicked",
|
||||||
|
"button_quadruple_press": "\"{subtype}\" quadruple clicked",
|
||||||
|
"button_quintuple_press": "\"{subtype}\" quintuple clicked"
|
||||||
|
},
|
||||||
|
"trigger_subtype": {
|
||||||
|
"turn_on": "Turn on",
|
||||||
|
"turn_off": "Turn off",
|
||||||
|
"button_1": "First button",
|
||||||
|
"button_2": "Second button",
|
||||||
|
"button_3": "Third button",
|
||||||
|
"button_4": "Fourth button",
|
||||||
|
"button_5": "Fifth button",
|
||||||
|
"button_6": "Sixth button"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"domain": "nsw_rural_fire_service_feed",
|
"domain": "nsw_rural_fire_service_feed",
|
||||||
"name": "NSW Rural Fire Service Incidents",
|
"name": "NSW Rural Fire Service Incidents",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed",
|
"documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed",
|
||||||
"requirements": ["aio_geojson_nsw_rfs_incidents==0.1"],
|
"requirements": ["aio_geojson_nsw_rfs_incidents==0.3"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@exxamalte"]
|
"codeowners": ["@exxamalte"]
|
||||||
}
|
}
|
||||||
|
@@ -4,5 +4,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/octoprint",
|
"documentation": "https://www.home-assistant.io/integrations/octoprint",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
|
"after_dependencies": ["discovery"],
|
||||||
"codeowners": []
|
"codeowners": []
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
"all_configured": "Tutti i server collegati sono gi\u00e0 configurati",
|
"all_configured": "Tutti i server collegati sono gi\u00e0 configurati",
|
||||||
"already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato",
|
"already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato",
|
||||||
"already_in_progress": "Plex \u00e8 in fase di configurazione",
|
"already_in_progress": "Plex \u00e8 in fase di configurazione",
|
||||||
"discovery_no_file": "Nessun file di configurazione ereditario trovato",
|
"discovery_no_file": "Non \u00e8 stato trovato nessun file di configurazione da sostituire",
|
||||||
"invalid_import": "La configurazione importata non \u00e8 valida",
|
"invalid_import": "La configurazione importata non \u00e8 valida",
|
||||||
"non-interactive": "Importazione non interattiva",
|
"non-interactive": "Importazione non interattiva",
|
||||||
"token_request_timeout": "Timeout per l'ottenimento del token",
|
"token_request_timeout": "Timeout per l'ottenimento del token",
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
"all_configured": "Alle knyttet servere som allerede er konfigurert",
|
"all_configured": "Alle knyttet servere som allerede er konfigurert",
|
||||||
"already_configured": "Denne Plex-serveren er allerede konfigurert",
|
"already_configured": "Denne Plex-serveren er allerede konfigurert",
|
||||||
"already_in_progress": "Plex blir konfigurert",
|
"already_in_progress": "Plex blir konfigurert",
|
||||||
"discovery_no_file": "Ingen eldre konfigurasjonsfil ble funnet",
|
"discovery_no_file": "Ingen eldre konfigurasjonsfil funnet",
|
||||||
"invalid_import": "Den importerte konfigurasjonen er ugyldig",
|
"invalid_import": "Den importerte konfigurasjonen er ugyldig",
|
||||||
"non-interactive": "Ikke-interaktiv import",
|
"non-interactive": "Ikke-interaktiv import",
|
||||||
"token_request_timeout": "Tidsavbrudd ved innhenting av token",
|
"token_request_timeout": "Tidsavbrudd ved innhenting av token",
|
||||||
|
@@ -27,6 +27,7 @@ from homeassistant.helpers.dispatcher import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_IGNORE_NEW_SHARED_USERS,
|
||||||
CONF_SERVER,
|
CONF_SERVER,
|
||||||
CONF_SERVER_IDENTIFIER,
|
CONF_SERVER_IDENTIFIER,
|
||||||
CONF_SHOW_ALL_CONTROLS,
|
CONF_SHOW_ALL_CONTROLS,
|
||||||
@@ -50,6 +51,7 @@ MEDIA_PLAYER_SCHEMA = vol.Schema(
|
|||||||
{
|
{
|
||||||
vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean,
|
vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean,
|
||||||
vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean,
|
vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean,
|
||||||
|
vol.Optional(CONF_IGNORE_NEW_SHARED_USERS, default=False): cv.boolean,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -14,12 +14,15 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
|||||||
from homeassistant.const import CONF_SSL, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
|
from homeassistant.const import CONF_SSL, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util.json import load_json
|
from homeassistant.util.json import load_json
|
||||||
|
|
||||||
from .const import ( # pylint: disable=unused-import
|
from .const import ( # pylint: disable=unused-import
|
||||||
AUTH_CALLBACK_NAME,
|
AUTH_CALLBACK_NAME,
|
||||||
AUTH_CALLBACK_PATH,
|
AUTH_CALLBACK_PATH,
|
||||||
CONF_CLIENT_IDENTIFIER,
|
CONF_CLIENT_IDENTIFIER,
|
||||||
|
CONF_IGNORE_NEW_SHARED_USERS,
|
||||||
|
CONF_MONITORED_USERS,
|
||||||
CONF_SERVER,
|
CONF_SERVER,
|
||||||
CONF_SERVER_IDENTIFIER,
|
CONF_SERVER_IDENTIFIER,
|
||||||
CONF_SHOW_ALL_CONTROLS,
|
CONF_SHOW_ALL_CONTROLS,
|
||||||
@@ -28,6 +31,7 @@ from .const import ( # pylint: disable=unused-import
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
PLEX_CONFIG_FILE,
|
PLEX_CONFIG_FILE,
|
||||||
PLEX_SERVER_CONFIG,
|
PLEX_SERVER_CONFIG,
|
||||||
|
SERVERS,
|
||||||
X_PLEX_DEVICE_NAME,
|
X_PLEX_DEVICE_NAME,
|
||||||
X_PLEX_PLATFORM,
|
X_PLEX_PLATFORM,
|
||||||
X_PLEX_PRODUCT,
|
X_PLEX_PRODUCT,
|
||||||
@@ -254,6 +258,7 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
def __init__(self, config_entry):
|
def __init__(self, config_entry):
|
||||||
"""Initialize Plex options flow."""
|
"""Initialize Plex options flow."""
|
||||||
self.options = copy.deepcopy(config_entry.options)
|
self.options = copy.deepcopy(config_entry.options)
|
||||||
|
self.server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
|
||||||
|
|
||||||
async def async_step_init(self, user_input=None):
|
async def async_step_init(self, user_input=None):
|
||||||
"""Manage the Plex options."""
|
"""Manage the Plex options."""
|
||||||
@@ -261,6 +266,8 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
|
|
||||||
async def async_step_plex_mp_settings(self, user_input=None):
|
async def async_step_plex_mp_settings(self, user_input=None):
|
||||||
"""Manage the Plex media_player options."""
|
"""Manage the Plex media_player options."""
|
||||||
|
plex_server = self.hass.data[DOMAIN][SERVERS][self.server_id]
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] = user_input[
|
self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] = user_input[
|
||||||
CONF_USE_EPISODE_ART
|
CONF_USE_EPISODE_ART
|
||||||
@@ -268,19 +275,56 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] = user_input[
|
self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] = user_input[
|
||||||
CONF_SHOW_ALL_CONTROLS
|
CONF_SHOW_ALL_CONTROLS
|
||||||
]
|
]
|
||||||
|
self.options[MP_DOMAIN][CONF_IGNORE_NEW_SHARED_USERS] = user_input[
|
||||||
|
CONF_IGNORE_NEW_SHARED_USERS
|
||||||
|
]
|
||||||
|
|
||||||
|
account_data = {
|
||||||
|
user: {"enabled": bool(user in user_input[CONF_MONITORED_USERS])}
|
||||||
|
for user in plex_server.accounts
|
||||||
|
}
|
||||||
|
|
||||||
|
self.options[MP_DOMAIN][CONF_MONITORED_USERS] = account_data
|
||||||
|
|
||||||
return self.async_create_entry(title="", data=self.options)
|
return self.async_create_entry(title="", data=self.options)
|
||||||
|
|
||||||
|
available_accounts = {name: name for name in plex_server.accounts}
|
||||||
|
available_accounts[plex_server.owner] += " [Owner]"
|
||||||
|
|
||||||
|
default_accounts = plex_server.accounts
|
||||||
|
known_accounts = set(plex_server.option_monitored_users)
|
||||||
|
if known_accounts:
|
||||||
|
default_accounts = {
|
||||||
|
user
|
||||||
|
for user in plex_server.option_monitored_users
|
||||||
|
if plex_server.option_monitored_users[user]["enabled"]
|
||||||
|
}
|
||||||
|
for user in plex_server.accounts:
|
||||||
|
if user not in known_accounts:
|
||||||
|
available_accounts[user] += " [New]"
|
||||||
|
|
||||||
|
if not plex_server.option_ignore_new_shared_users:
|
||||||
|
for new_user in plex_server.accounts - known_accounts:
|
||||||
|
default_accounts.add(new_user)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="plex_mp_settings",
|
step_id="plex_mp_settings",
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_USE_EPISODE_ART,
|
CONF_USE_EPISODE_ART,
|
||||||
default=self.options[MP_DOMAIN][CONF_USE_EPISODE_ART],
|
default=plex_server.option_use_episode_art,
|
||||||
): bool,
|
): bool,
|
||||||
vol.Required(
|
vol.Required(
|
||||||
CONF_SHOW_ALL_CONTROLS,
|
CONF_SHOW_ALL_CONTROLS,
|
||||||
default=self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS],
|
default=plex_server.option_show_all_controls,
|
||||||
|
): bool,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MONITORED_USERS, default=default_accounts
|
||||||
|
): cv.multi_select(available_accounts),
|
||||||
|
vol.Required(
|
||||||
|
CONF_IGNORE_NEW_SHARED_USERS,
|
||||||
|
default=plex_server.option_ignore_new_shared_users,
|
||||||
): bool,
|
): bool,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@@ -29,6 +29,8 @@ CONF_SERVER = "server"
|
|||||||
CONF_SERVER_IDENTIFIER = "server_id"
|
CONF_SERVER_IDENTIFIER = "server_id"
|
||||||
CONF_USE_EPISODE_ART = "use_episode_art"
|
CONF_USE_EPISODE_ART = "use_episode_art"
|
||||||
CONF_SHOW_ALL_CONTROLS = "show_all_controls"
|
CONF_SHOW_ALL_CONTROLS = "show_all_controls"
|
||||||
|
CONF_IGNORE_NEW_SHARED_USERS = "ignore_new_shared_users"
|
||||||
|
CONF_MONITORED_USERS = "monitored_users"
|
||||||
|
|
||||||
AUTH_CALLBACK_PATH = "/auth/plex/callback"
|
AUTH_CALLBACK_PATH = "/auth/plex/callback"
|
||||||
AUTH_CALLBACK_NAME = "auth:plex:callback"
|
AUTH_CALLBACK_NAME = "auth:plex:callback"
|
||||||
|
@@ -218,6 +218,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
|
|||||||
if session_device:
|
if session_device:
|
||||||
self._make = session_device.device or ""
|
self._make = session_device.device or ""
|
||||||
self._player_state = session_device.state
|
self._player_state = session_device.state
|
||||||
|
self._device_platform = self._device_platform or session_device.platform
|
||||||
self._device_product = self._device_product or session_device.product
|
self._device_product = self._device_product or session_device.product
|
||||||
self._device_title = self._device_title or session_device.title
|
self._device_title = self._device_title or session_device.title
|
||||||
self._device_version = self._device_version or session_device.version
|
self._device_version = self._device_version or session_device.version
|
||||||
@@ -243,7 +244,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
|
|||||||
self._media_content_id = self.session.ratingKey
|
self._media_content_id = self.session.ratingKey
|
||||||
self._media_content_rating = getattr(self.session, "contentRating", None)
|
self._media_content_rating = getattr(self.session, "contentRating", None)
|
||||||
|
|
||||||
name_parts = [self._device_product, self._device_title]
|
name_parts = [self._device_product, self._device_title or self._device_platform]
|
||||||
if (self._device_product in COMMON_PLAYERS) and self.make:
|
if (self._device_product in COMMON_PLAYERS) and self.make:
|
||||||
# Add more context in name for likely duplicates
|
# Add more context in name for likely duplicates
|
||||||
name_parts.append(self.make)
|
name_parts.append(self.make)
|
||||||
@@ -274,7 +275,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
|
|||||||
thumb_url = self.session.thumbUrl
|
thumb_url = self.session.thumbUrl
|
||||||
if (
|
if (
|
||||||
self.media_content_type is MEDIA_TYPE_TVSHOW
|
self.media_content_type is MEDIA_TYPE_TVSHOW
|
||||||
and not self.plex_server.use_episode_art
|
and not self.plex_server.option_use_episode_art
|
||||||
):
|
):
|
||||||
thumb_url = self.session.url(self.session.grandparentThumb)
|
thumb_url = self.session.url(self.session.grandparentThumb)
|
||||||
|
|
||||||
@@ -481,7 +482,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
|
|||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Flag media player features that are supported."""
|
"""Flag media player features that are supported."""
|
||||||
# force show all controls
|
# force show all controls
|
||||||
if self.plex_server.show_all_controls:
|
if self.plex_server.option_show_all_controls:
|
||||||
return (
|
return (
|
||||||
SUPPORT_PAUSE
|
SUPPORT_PAUSE
|
||||||
| SUPPORT_PREVIOUS_TRACK
|
| SUPPORT_PREVIOUS_TRACK
|
||||||
@@ -738,8 +739,8 @@ class PlexMediaPlayer(MediaPlayerDevice):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"identifiers": {(PLEX_DOMAIN, self.machine_identifier)},
|
"identifiers": {(PLEX_DOMAIN, self.machine_identifier)},
|
||||||
"manufacturer": "Plex",
|
"manufacturer": self._device_platform or "Plex",
|
||||||
"model": self._device_product or self._device_platform or self.make,
|
"model": self._device_product or self.make,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"sw_version": self._device_version,
|
"sw_version": self._device_version,
|
||||||
"via_device": (PLEX_DOMAIN, self.plex_server.machine_identifier),
|
"via_device": (PLEX_DOMAIN, self.plex_server.machine_identifier),
|
||||||
|
@@ -13,6 +13,8 @@ from homeassistant.helpers.dispatcher import dispatcher_send
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_CLIENT_IDENTIFIER,
|
CONF_CLIENT_IDENTIFIER,
|
||||||
|
CONF_IGNORE_NEW_SHARED_USERS,
|
||||||
|
CONF_MONITORED_USERS,
|
||||||
CONF_SERVER,
|
CONF_SERVER,
|
||||||
CONF_SHOW_ALL_CONTROLS,
|
CONF_SHOW_ALL_CONTROLS,
|
||||||
CONF_USE_EPISODE_ART,
|
CONF_USE_EPISODE_ART,
|
||||||
@@ -51,6 +53,7 @@ class PlexServer:
|
|||||||
self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
|
self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
|
||||||
self.options = options
|
self.options = options
|
||||||
self.server_choice = None
|
self.server_choice = None
|
||||||
|
self._accounts = []
|
||||||
self._owner_username = None
|
self._owner_username = None
|
||||||
self._version = None
|
self._version = None
|
||||||
|
|
||||||
@@ -95,6 +98,12 @@ class PlexServer:
|
|||||||
else:
|
else:
|
||||||
_connect_with_token()
|
_connect_with_token()
|
||||||
|
|
||||||
|
self._accounts = [
|
||||||
|
account.name
|
||||||
|
for account in self._plex_server.systemAccounts()
|
||||||
|
if account.name
|
||||||
|
]
|
||||||
|
|
||||||
owner_account = [
|
owner_account = [
|
||||||
account.name
|
account.name
|
||||||
for account in self._plex_server.systemAccounts()
|
for account in self._plex_server.systemAccounts()
|
||||||
@@ -121,8 +130,22 @@ class PlexServer:
|
|||||||
_LOGGER.debug("Updating devices")
|
_LOGGER.debug("Updating devices")
|
||||||
|
|
||||||
available_clients = {}
|
available_clients = {}
|
||||||
|
ignored_clients = set()
|
||||||
new_clients = set()
|
new_clients = set()
|
||||||
|
|
||||||
|
monitored_users = self.accounts
|
||||||
|
known_accounts = set(self.option_monitored_users)
|
||||||
|
if known_accounts:
|
||||||
|
monitored_users = {
|
||||||
|
user
|
||||||
|
for user in self.option_monitored_users
|
||||||
|
if self.option_monitored_users[user]["enabled"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if not self.option_ignore_new_shared_users:
|
||||||
|
for new_user in self.accounts - known_accounts:
|
||||||
|
monitored_users.add(new_user)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
devices = self._plex_server.clients()
|
devices = self._plex_server.clients()
|
||||||
sessions = self._plex_server.sessions()
|
sessions = self._plex_server.sessions()
|
||||||
@@ -147,7 +170,12 @@ class PlexServer:
|
|||||||
if session.TYPE == "photo":
|
if session.TYPE == "photo":
|
||||||
_LOGGER.debug("Photo session detected, skipping: %s", session)
|
_LOGGER.debug("Photo session detected, skipping: %s", session)
|
||||||
continue
|
continue
|
||||||
|
session_username = session.usernames[0]
|
||||||
for player in session.players:
|
for player in session.players:
|
||||||
|
if session_username not in monitored_users:
|
||||||
|
ignored_clients.add(player.machineIdentifier)
|
||||||
|
_LOGGER.debug("Ignoring Plex client owned by %s", session_username)
|
||||||
|
continue
|
||||||
self._known_idle.discard(player.machineIdentifier)
|
self._known_idle.discard(player.machineIdentifier)
|
||||||
available_clients.setdefault(
|
available_clients.setdefault(
|
||||||
player.machineIdentifier, {"device": player}
|
player.machineIdentifier, {"device": player}
|
||||||
@@ -160,6 +188,8 @@ class PlexServer:
|
|||||||
|
|
||||||
new_entity_configs = []
|
new_entity_configs = []
|
||||||
for client_id, client_data in available_clients.items():
|
for client_id, client_data in available_clients.items():
|
||||||
|
if client_id in ignored_clients:
|
||||||
|
continue
|
||||||
if client_id in new_clients:
|
if client_id in new_clients:
|
||||||
new_entity_configs.append(client_data)
|
new_entity_configs.append(client_data)
|
||||||
else:
|
else:
|
||||||
@@ -167,11 +197,11 @@ class PlexServer:
|
|||||||
client_id, client_data["device"], client_data.get("session")
|
client_id, client_data["device"], client_data.get("session")
|
||||||
)
|
)
|
||||||
|
|
||||||
self._known_clients.update(new_clients)
|
self._known_clients.update(new_clients | ignored_clients)
|
||||||
|
|
||||||
idle_clients = (self._known_clients - self._known_idle).difference(
|
idle_clients = (
|
||||||
available_clients
|
self._known_clients - self._known_idle - ignored_clients
|
||||||
)
|
).difference(available_clients)
|
||||||
for client_id in idle_clients:
|
for client_id in idle_clients:
|
||||||
self.refresh_entity(client_id, None, None)
|
self.refresh_entity(client_id, None, None)
|
||||||
self._known_idle.add(client_id)
|
self._known_idle.add(client_id)
|
||||||
@@ -194,6 +224,11 @@ class PlexServer:
|
|||||||
"""Return the plexapi PlexServer instance."""
|
"""Return the plexapi PlexServer instance."""
|
||||||
return self._plex_server
|
return self._plex_server
|
||||||
|
|
||||||
|
@property
|
||||||
|
def accounts(self):
|
||||||
|
"""Return accounts associated with the Plex server."""
|
||||||
|
return set(self._accounts)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def owner(self):
|
def owner(self):
|
||||||
"""Return the Plex server owner username."""
|
"""Return the Plex server owner username."""
|
||||||
@@ -220,15 +255,25 @@ class PlexServer:
|
|||||||
return self._plex_server._baseurl # pylint: disable=protected-access
|
return self._plex_server._baseurl # pylint: disable=protected-access
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def use_episode_art(self):
|
def option_ignore_new_shared_users(self):
|
||||||
|
"""Return ignore_new_shared_users option."""
|
||||||
|
return self.options[MP_DOMAIN].get(CONF_IGNORE_NEW_SHARED_USERS, False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def option_use_episode_art(self):
|
||||||
"""Return use_episode_art option."""
|
"""Return use_episode_art option."""
|
||||||
return self.options[MP_DOMAIN][CONF_USE_EPISODE_ART]
|
return self.options[MP_DOMAIN][CONF_USE_EPISODE_ART]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def show_all_controls(self):
|
def option_show_all_controls(self):
|
||||||
"""Return show_all_controls option."""
|
"""Return show_all_controls option."""
|
||||||
return self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS]
|
return self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def option_monitored_users(self):
|
||||||
|
"""Return dict of monitored users option."""
|
||||||
|
return self.options[MP_DOMAIN].get(CONF_MONITORED_USERS, {})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def library(self):
|
def library(self):
|
||||||
"""Return library attribute from server object."""
|
"""Return library attribute from server object."""
|
||||||
|
@@ -36,7 +36,9 @@
|
|||||||
"description": "Options for Plex Media Players",
|
"description": "Options for Plex Media Players",
|
||||||
"data": {
|
"data": {
|
||||||
"use_episode_art": "Use episode art",
|
"use_episode_art": "Use episode art",
|
||||||
"show_all_controls": "Show all controls"
|
"show_all_controls": "Show all controls",
|
||||||
|
"ignore_new_shared_users": "Ignore new managed/shared users",
|
||||||
|
"monitored_users": "Monitored users"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
"name": "Sony PlayStation 4",
|
"name": "Sony PlayStation 4",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ps4",
|
"documentation": "https://www.home-assistant.io/integrations/ps4",
|
||||||
"requirements": ["pyps4-2ndscreen==1.0.6"],
|
"requirements": ["pyps4-2ndscreen==1.0.7"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@ktnrg45"]
|
"codeowners": ["@ktnrg45"]
|
||||||
}
|
}
|
||||||
|
@@ -4,5 +4,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/roku",
|
"documentation": "https://www.home-assistant.io/integrations/roku",
|
||||||
"requirements": ["roku==4.0.0"],
|
"requirements": ["roku==4.0.0"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
|
"after_dependencies": ["discovery"],
|
||||||
"codeowners": []
|
"codeowners": []
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,7 @@ from homeassistant.components.light import (
|
|||||||
SUPPORT_TRANSITION,
|
SUPPORT_TRANSITION,
|
||||||
Light,
|
Light,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE, STATE_ON
|
from homeassistant.const import CONF_ADDRESS, CONF_HOST, CONF_NAME, CONF_TYPE, STATE_ON
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
@@ -58,6 +58,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
vol.Required(CONF_TYPE): vol.In(CONF_LED_TYPES),
|
vol.Required(CONF_TYPE): vol.In(CONF_LED_TYPES),
|
||||||
vol.Optional(CONF_FREQUENCY): cv.positive_int,
|
vol.Optional(CONF_FREQUENCY): cv.positive_int,
|
||||||
vol.Optional(CONF_ADDRESS): cv.byte,
|
vol.Optional(CONF_ADDRESS): cv.byte,
|
||||||
|
vol.Optional(CONF_HOST): cv.string,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -76,6 +77,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
if CONF_FREQUENCY in led_conf:
|
if CONF_FREQUENCY in led_conf:
|
||||||
opt_args["freq"] = led_conf[CONF_FREQUENCY]
|
opt_args["freq"] = led_conf[CONF_FREQUENCY]
|
||||||
if driver_type == CONF_DRIVER_GPIO:
|
if driver_type == CONF_DRIVER_GPIO:
|
||||||
|
if CONF_HOST in led_conf:
|
||||||
|
opt_args["host"] = led_conf[CONF_HOST]
|
||||||
driver = GpioDriver(pins, **opt_args)
|
driver = GpioDriver(pins, **opt_args)
|
||||||
elif driver_type == CONF_DRIVER_PCA9685:
|
elif driver_type == CONF_DRIVER_PCA9685:
|
||||||
if CONF_ADDRESS in led_conf:
|
if CONF_ADDRESS in led_conf:
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"domain": "rpi_gpio_pwm",
|
"domain": "rpi_gpio_pwm",
|
||||||
"name": "pigpio Daemon PWM LED",
|
"name": "pigpio Daemon PWM LED",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm",
|
"documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm",
|
||||||
"requirements": ["pwmled==1.4.1"],
|
"requirements": ["pwmled==1.5.0"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": []
|
"codeowners": []
|
||||||
}
|
}
|
||||||
|
@@ -4,5 +4,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/sabnzbd",
|
"documentation": "https://www.home-assistant.io/integrations/sabnzbd",
|
||||||
"requirements": ["pysabnzbd==1.1.0"],
|
"requirements": ["pysabnzbd==1.1.0"],
|
||||||
"dependencies": ["configurator"],
|
"dependencies": ["configurator"],
|
||||||
|
"after_dependencies": ["discovery"],
|
||||||
"codeowners": []
|
"codeowners": []
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,13 @@ from simplipy.websocket import (
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
|
from homeassistant.const import (
|
||||||
|
ATTR_CODE,
|
||||||
|
CONF_CODE,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_TOKEN,
|
||||||
|
CONF_USERNAME,
|
||||||
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
@@ -59,6 +65,7 @@ DATA_LISTENER = "listener"
|
|||||||
TOPIC_UPDATE = "simplisafe_update_data_{0}"
|
TOPIC_UPDATE = "simplisafe_update_data_{0}"
|
||||||
|
|
||||||
EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT"
|
EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT"
|
||||||
|
EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION"
|
||||||
|
|
||||||
DEFAULT_SOCKET_MIN_RETRY = 15
|
DEFAULT_SOCKET_MIN_RETRY = 15
|
||||||
DEFAULT_WATCHDOG_SECONDS = 5 * 60
|
DEFAULT_WATCHDOG_SECONDS = 5 * 60
|
||||||
@@ -71,6 +78,7 @@ WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT = [
|
|||||||
EVENT_MOTION_DETECTED,
|
EVENT_MOTION_DETECTED,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ATTR_CATEGORY = "category"
|
||||||
ATTR_LAST_EVENT_CHANGED_BY = "last_event_changed_by"
|
ATTR_LAST_EVENT_CHANGED_BY = "last_event_changed_by"
|
||||||
ATTR_LAST_EVENT_INFO = "last_event_info"
|
ATTR_LAST_EVENT_INFO = "last_event_info"
|
||||||
ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name"
|
ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name"
|
||||||
@@ -79,10 +87,12 @@ ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type"
|
|||||||
ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp"
|
ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp"
|
||||||
ATTR_LAST_EVENT_TYPE = "last_event_type"
|
ATTR_LAST_EVENT_TYPE = "last_event_type"
|
||||||
ATTR_LAST_EVENT_TYPE = "last_event_type"
|
ATTR_LAST_EVENT_TYPE = "last_event_type"
|
||||||
|
ATTR_MESSAGE = "message"
|
||||||
ATTR_PIN_LABEL = "label"
|
ATTR_PIN_LABEL = "label"
|
||||||
ATTR_PIN_LABEL_OR_VALUE = "label_or_pin"
|
ATTR_PIN_LABEL_OR_VALUE = "label_or_pin"
|
||||||
ATTR_PIN_VALUE = "pin"
|
ATTR_PIN_VALUE = "pin"
|
||||||
ATTR_SYSTEM_ID = "system_id"
|
ATTR_SYSTEM_ID = "system_id"
|
||||||
|
ATTR_TIMESTAMP = "timestamp"
|
||||||
|
|
||||||
SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int})
|
SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int})
|
||||||
|
|
||||||
@@ -428,10 +438,43 @@ class SimpliSafe:
|
|||||||
self._config_entry = config_entry
|
self._config_entry = config_entry
|
||||||
self._emergency_refresh_token_used = False
|
self._emergency_refresh_token_used = False
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
self._system_notifications = {}
|
||||||
self.initial_event_to_use = {}
|
self.initial_event_to_use = {}
|
||||||
self.systems = None
|
self.systems = {}
|
||||||
self.websocket = SimpliSafeWebsocket(hass, api.websocket)
|
self.websocket = SimpliSafeWebsocket(hass, api.websocket)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_process_new_notifications(self, system):
|
||||||
|
"""Act on any new system notifications."""
|
||||||
|
old_notifications = self._system_notifications.get(system.system_id, [])
|
||||||
|
latest_notifications = system.notifications
|
||||||
|
|
||||||
|
# Save the latest notifications:
|
||||||
|
self._system_notifications[system.system_id] = latest_notifications
|
||||||
|
|
||||||
|
# Process any notifications that are new:
|
||||||
|
to_add = set(latest_notifications) - set(old_notifications)
|
||||||
|
|
||||||
|
if not to_add:
|
||||||
|
return
|
||||||
|
|
||||||
|
_LOGGER.debug("New system notifications: %s", to_add)
|
||||||
|
|
||||||
|
for notification in to_add:
|
||||||
|
text = notification.text
|
||||||
|
if notification.link:
|
||||||
|
text = f"{text} For more information: {notification.link}"
|
||||||
|
|
||||||
|
self._hass.bus.async_fire(
|
||||||
|
EVENT_SIMPLISAFE_NOTIFICATION,
|
||||||
|
event_data={
|
||||||
|
ATTR_CATEGORY: notification.category,
|
||||||
|
ATTR_CODE: notification.code,
|
||||||
|
ATTR_MESSAGE: text,
|
||||||
|
ATTR_TIMESTAMP: notification.timestamp,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
async def async_init(self):
|
async def async_init(self):
|
||||||
"""Initialize the data class."""
|
"""Initialize the data class."""
|
||||||
asyncio.create_task(self.websocket.async_websocket_connect())
|
asyncio.create_task(self.websocket.async_websocket_connect())
|
||||||
@@ -471,6 +514,7 @@ class SimpliSafe:
|
|||||||
async def update_system(system):
|
async def update_system(system):
|
||||||
"""Update a system."""
|
"""Update a system."""
|
||||||
await system.update()
|
await system.update()
|
||||||
|
self._async_process_new_notifications(system)
|
||||||
_LOGGER.debug('Updated REST API data for "%s"', system.address)
|
_LOGGER.debug('Updated REST API data for "%s"', system.address)
|
||||||
async_dispatcher_send(self._hass, TOPIC_UPDATE.format(system.system_id))
|
async_dispatcher_send(self._hass, TOPIC_UPDATE.format(system.system_id))
|
||||||
|
|
||||||
|
@@ -406,7 +406,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateDevice):
|
|||||||
self._device.device_id,
|
self._device.device_id,
|
||||||
mode,
|
mode,
|
||||||
)
|
)
|
||||||
self._hvac_modes = modes
|
self._hvac_modes = list(modes)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self):
|
def current_temperature(self):
|
||||||
|
@@ -6,6 +6,7 @@ from twitch import TwitchClient
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_TOKEN
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
@@ -13,6 +14,13 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
ATTR_GAME = "game"
|
ATTR_GAME = "game"
|
||||||
ATTR_TITLE = "title"
|
ATTR_TITLE = "title"
|
||||||
|
ATTR_SUBSCRIPTION = "subscribed"
|
||||||
|
ATTR_SUBSCRIPTION_SINCE = "subscribed_since"
|
||||||
|
ATTR_SUBSCRIPTION_GIFTED = "subscription_is_gifted"
|
||||||
|
ATTR_FOLLOW = "following"
|
||||||
|
ATTR_FOLLOW_SINCE = "following_since"
|
||||||
|
ATTR_FOLLOWING = "followers"
|
||||||
|
ATTR_VIEWS = "views"
|
||||||
|
|
||||||
CONF_CHANNELS = "channels"
|
CONF_CHANNELS = "channels"
|
||||||
CONF_CLIENT_ID = "client_id"
|
CONF_CLIENT_ID = "client_id"
|
||||||
@@ -26,6 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
{
|
{
|
||||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||||
vol.Required(CONF_CHANNELS): vol.All(cv.ensure_list, [cv.string]),
|
vol.Required(CONF_CHANNELS): vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
vol.Optional(CONF_TOKEN): cv.string,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,29 +43,35 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
"""Set up the Twitch platform."""
|
"""Set up the Twitch platform."""
|
||||||
channels = config[CONF_CHANNELS]
|
channels = config[CONF_CHANNELS]
|
||||||
client_id = config[CONF_CLIENT_ID]
|
client_id = config[CONF_CLIENT_ID]
|
||||||
client = TwitchClient(client_id=client_id)
|
oauth_token = config.get(CONF_TOKEN)
|
||||||
|
client = TwitchClient(client_id, oauth_token)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client.ingests.get_server_list()
|
client.ingests.get_server_list()
|
||||||
except HTTPError:
|
except HTTPError:
|
||||||
_LOGGER.error("Client ID is not valid")
|
_LOGGER.error("Client ID or OAuth token is not valid")
|
||||||
return
|
return
|
||||||
|
|
||||||
users = client.users.translate_usernames_to_ids(channels)
|
channel_ids = client.users.translate_usernames_to_ids(channels)
|
||||||
|
|
||||||
add_entities([TwitchSensor(user, client) for user in users], True)
|
add_entities([TwitchSensor(channel_id, client) for channel_id in channel_ids], True)
|
||||||
|
|
||||||
|
|
||||||
class TwitchSensor(Entity):
|
class TwitchSensor(Entity):
|
||||||
"""Representation of an Twitch channel."""
|
"""Representation of an Twitch channel."""
|
||||||
|
|
||||||
def __init__(self, user, client):
|
def __init__(self, channel, client):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._client = client
|
self._client = client
|
||||||
self._user = user
|
self._channel = channel
|
||||||
self._channel = self._user.name
|
self._oauth_enabled = client._oauth_token is not None
|
||||||
self._id = self._user.id
|
self._state = None
|
||||||
self._state = self._preview = self._game = self._title = None
|
self._preview = None
|
||||||
|
self._game = None
|
||||||
|
self._title = None
|
||||||
|
self._subscription = None
|
||||||
|
self._follow = None
|
||||||
|
self._statistics = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
@@ -66,7 +81,7 @@ class TwitchSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
return self._channel
|
return self._channel.display_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
@@ -81,28 +96,67 @@ class TwitchSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
|
attr = {
|
||||||
|
ATTR_FRIENDLY_NAME: self._channel.display_name,
|
||||||
|
}
|
||||||
|
attr.update(self._statistics)
|
||||||
|
|
||||||
|
if self._oauth_enabled:
|
||||||
|
attr.update(self._subscription)
|
||||||
|
attr.update(self._follow)
|
||||||
|
|
||||||
if self._state == STATE_STREAMING:
|
if self._state == STATE_STREAMING:
|
||||||
return {ATTR_GAME: self._game, ATTR_TITLE: self._title}
|
attr.update({ATTR_GAME: self._game, ATTR_TITLE: self._title})
|
||||||
|
return attr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return unique ID for this sensor."""
|
"""Return unique ID for this sensor."""
|
||||||
return self._id
|
return self._channel.id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
"""Icon to use in the frontend, if any."""
|
"""Icon to use in the frontend, if any."""
|
||||||
return ICON
|
return ICON
|
||||||
|
|
||||||
# pylint: disable=no-member
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update device state."""
|
"""Update device state."""
|
||||||
stream = self._client.streams.get_stream_by_user(self._id)
|
|
||||||
|
channel = self._client.channels.get_by_id(self._channel.id)
|
||||||
|
|
||||||
|
self._statistics = {
|
||||||
|
ATTR_FOLLOWING: channel.followers,
|
||||||
|
ATTR_VIEWS: channel.views,
|
||||||
|
}
|
||||||
|
if self._oauth_enabled:
|
||||||
|
user = self._client.users.get()
|
||||||
|
|
||||||
|
try:
|
||||||
|
sub = self._client.users.check_subscribed_to_channel(
|
||||||
|
user.id, self._channel.id
|
||||||
|
)
|
||||||
|
self._subscription = {
|
||||||
|
ATTR_SUBSCRIPTION: True,
|
||||||
|
ATTR_SUBSCRIPTION_SINCE: sub.created_at,
|
||||||
|
ATTR_SUBSCRIPTION_GIFTED: sub.is_gift,
|
||||||
|
}
|
||||||
|
except HTTPError:
|
||||||
|
self._subscription = {ATTR_SUBSCRIPTION: False}
|
||||||
|
|
||||||
|
try:
|
||||||
|
follow = self._client.users.check_follows_channel(
|
||||||
|
user.id, self._channel.id
|
||||||
|
)
|
||||||
|
self._follow = {ATTR_FOLLOW: True, ATTR_FOLLOW_SINCE: follow.created_at}
|
||||||
|
except HTTPError:
|
||||||
|
self._follow = {ATTR_FOLLOW: False}
|
||||||
|
|
||||||
|
stream = self._client.streams.get_stream_by_user(self._channel.id)
|
||||||
if stream:
|
if stream:
|
||||||
self._game = stream.get("channel").get("game")
|
self._game = stream.channel.get("game")
|
||||||
self._title = stream.get("channel").get("status")
|
self._title = stream.channel.get("status")
|
||||||
self._preview = stream.get("preview").get("medium")
|
self._preview = stream.preview.get("medium")
|
||||||
self._state = STATE_STREAMING
|
self._state = STATE_STREAMING
|
||||||
else:
|
else:
|
||||||
self._preview = self._client.users.get_by_id(self._id).get("logo")
|
self._preview = self._channel.logo
|
||||||
self._state = STATE_OFFLINE
|
self._state = STATE_OFFLINE
|
||||||
|
@@ -28,15 +28,20 @@
|
|||||||
"device_tracker": {
|
"device_tracker": {
|
||||||
"data": {
|
"data": {
|
||||||
"detection_time": "Time in seconds from last seen until considered away",
|
"detection_time": "Time in seconds from last seen until considered away",
|
||||||
|
"ssid_filter": "Select SSIDs to track wireless clients on",
|
||||||
"track_clients": "Track network clients",
|
"track_clients": "Track network clients",
|
||||||
"track_devices": "Track network devices (Ubiquiti devices)",
|
"track_devices": "Track network devices (Ubiquiti devices)",
|
||||||
"track_wired_clients": "Include wired network clients"
|
"track_wired_clients": "Include wired network clients"
|
||||||
}
|
},
|
||||||
|
"description": "Configure device tracking",
|
||||||
|
"title": "UniFi options"
|
||||||
},
|
},
|
||||||
"statistics_sensors": {
|
"statistics_sensors": {
|
||||||
"data": {
|
"data": {
|
||||||
"allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients"
|
"allow_bandwidth_sensors": "Bandwidth usage sensors for network clients"
|
||||||
}
|
},
|
||||||
|
"description": "Configure statistics sensors",
|
||||||
|
"title": "UniFi options"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,12 +33,6 @@
|
|||||||
"track_wired_clients": "Inkluder kablede nettverksklienter"
|
"track_wired_clients": "Inkluder kablede nettverksklienter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"init": {
|
|
||||||
"data": {
|
|
||||||
"one": "en",
|
|
||||||
"other": "andre"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"statistics_sensors": {
|
"statistics_sensors": {
|
||||||
"data": {
|
"data": {
|
||||||
"allow_bandwidth_sensors": "Opprett b\u00e5ndbreddesensorer for nettverksklienter"
|
"allow_bandwidth_sensors": "Opprett b\u00e5ndbreddesensorer for nettverksklienter"
|
||||||
|
@@ -36,7 +36,7 @@
|
|||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
"one": "Tom",
|
"one": "Tom",
|
||||||
"other": "Tom"
|
"other": "Tomma"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"statistics_sensors": {
|
"statistics_sensors": {
|
||||||
|
@@ -12,21 +12,18 @@ from homeassistant.const import (
|
|||||||
CONF_VERIFY_SSL,
|
CONF_VERIFY_SSL,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ALLOW_BANDWIDTH_SENSORS,
|
CONF_ALLOW_BANDWIDTH_SENSORS,
|
||||||
CONF_CONTROLLER,
|
CONF_CONTROLLER,
|
||||||
CONF_DETECTION_TIME,
|
CONF_DETECTION_TIME,
|
||||||
CONF_SITE_ID,
|
CONF_SITE_ID,
|
||||||
|
CONF_SSID_FILTER,
|
||||||
CONF_TRACK_CLIENTS,
|
CONF_TRACK_CLIENTS,
|
||||||
CONF_TRACK_DEVICES,
|
CONF_TRACK_DEVICES,
|
||||||
CONF_TRACK_WIRED_CLIENTS,
|
CONF_TRACK_WIRED_CLIENTS,
|
||||||
CONTROLLER_ID,
|
CONTROLLER_ID,
|
||||||
DEFAULT_ALLOW_BANDWIDTH_SENSORS,
|
|
||||||
DEFAULT_DETECTION_TIME,
|
|
||||||
DEFAULT_TRACK_CLIENTS,
|
|
||||||
DEFAULT_TRACK_DEVICES,
|
|
||||||
DEFAULT_TRACK_WIRED_CLIENTS,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
)
|
)
|
||||||
@@ -185,33 +182,30 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
self.options.update(user_input)
|
self.options.update(user_input)
|
||||||
return await self.async_step_statistics_sensors()
|
return await self.async_step_statistics_sensors()
|
||||||
|
|
||||||
|
controller = get_controller_from_config_entry(self.hass, self.config_entry)
|
||||||
|
|
||||||
|
ssid_filter = {wlan: wlan for wlan in controller.api.wlans}
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="device_tracker",
|
step_id="device_tracker",
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_TRACK_CLIENTS,
|
CONF_TRACK_CLIENTS, default=controller.option_track_clients,
|
||||||
default=self.config_entry.options.get(
|
|
||||||
CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS
|
|
||||||
),
|
|
||||||
): bool,
|
): bool,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_TRACK_WIRED_CLIENTS,
|
CONF_TRACK_WIRED_CLIENTS,
|
||||||
default=self.config_entry.options.get(
|
default=controller.option_track_wired_clients,
|
||||||
CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS
|
|
||||||
),
|
|
||||||
): bool,
|
): bool,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_TRACK_DEVICES,
|
CONF_TRACK_DEVICES, default=controller.option_track_devices,
|
||||||
default=self.config_entry.options.get(
|
|
||||||
CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES
|
|
||||||
),
|
|
||||||
): bool,
|
): bool,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_SSID_FILTER, default=controller.option_ssid_filter
|
||||||
|
): cv.multi_select(ssid_filter),
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_DETECTION_TIME,
|
CONF_DETECTION_TIME,
|
||||||
default=self.config_entry.options.get(
|
default=int(controller.option_detection_time.total_seconds()),
|
||||||
CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME
|
|
||||||
),
|
|
||||||
): int,
|
): int,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -223,16 +217,15 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
self.options.update(user_input)
|
self.options.update(user_input)
|
||||||
return await self._update_options()
|
return await self._update_options()
|
||||||
|
|
||||||
|
controller = get_controller_from_config_entry(self.hass, self.config_entry)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="statistics_sensors",
|
step_id="statistics_sensors",
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_ALLOW_BANDWIDTH_SENSORS,
|
CONF_ALLOW_BANDWIDTH_SENSORS,
|
||||||
default=self.config_entry.options.get(
|
default=controller.option_allow_bandwidth_sensors,
|
||||||
CONF_ALLOW_BANDWIDTH_SENSORS,
|
|
||||||
DEFAULT_ALLOW_BANDWIDTH_SENSORS,
|
|
||||||
),
|
|
||||||
): bool
|
): bool
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/unifi",
|
"documentation": "https://www.home-assistant.io/integrations/unifi",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aiounifi==12"
|
"aiounifi==13"
|
||||||
],
|
],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
|
@@ -31,15 +31,20 @@
|
|||||||
"device_tracker": {
|
"device_tracker": {
|
||||||
"data": {
|
"data": {
|
||||||
"detection_time": "Time in seconds from last seen until considered away",
|
"detection_time": "Time in seconds from last seen until considered away",
|
||||||
|
"ssid_filter": "Select SSIDs to track wireless clients on",
|
||||||
"track_clients": "Track network clients",
|
"track_clients": "Track network clients",
|
||||||
"track_devices": "Track network devices (Ubiquiti devices)",
|
"track_devices": "Track network devices (Ubiquiti devices)",
|
||||||
"track_wired_clients": "Include wired network clients"
|
"track_wired_clients": "Include wired network clients"
|
||||||
}
|
},
|
||||||
|
"description": "Configure device tracking",
|
||||||
|
"title": "UniFi options"
|
||||||
},
|
},
|
||||||
"statistics_sensors": {
|
"statistics_sensors": {
|
||||||
"data": {
|
"data": {
|
||||||
"allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients"
|
"allow_bandwidth_sensors": "Bandwidth usage sensors for network clients"
|
||||||
}
|
},
|
||||||
|
"description": "Configure statistics sensors",
|
||||||
|
"title": "UniFi options"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
"domain": "vallox",
|
"domain": "vallox",
|
||||||
"name": "Valloxs",
|
"name": "Valloxs",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/vallox",
|
"documentation": "https://www.home-assistant.io/integrations/vallox",
|
||||||
"requirements": ["vallox-websocket-api==2.2.0"],
|
"requirements": ["vallox-websocket-api==2.4.0"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": []
|
"codeowners": []
|
||||||
}
|
}
|
||||||
|
@@ -1,11 +1,21 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Deze Vilfo Router is al geconfigureerd."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Kon niet verbinden. Controleer de door u verstrekte informatie en probeer het opnieuw.",
|
||||||
|
"unknown": "Er is een onverwachte fout opgetreden tijdens het instellen van de integratie."
|
||||||
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"access_token": "Toegangstoken voor de Vilfo Router API",
|
||||||
"host": "Router hostnaam of IP-adres"
|
"host": "Router hostnaam of IP-adres"
|
||||||
}
|
},
|
||||||
}
|
"title": "Maak verbinding met de Vilfo Router"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"title": "Vilfo Router"
|
||||||
}
|
}
|
||||||
}
|
}
|
23
homeassistant/components/vilfo/.translations/sl.json
Normal file
23
homeassistant/components/vilfo/.translations/sl.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Ta usmerjevalnik Vilfo je \u017ee konfiguriran."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Povezava ni uspela. Prosimo, preverite informacije, ki ste jih vnesli in poskusite znova.",
|
||||||
|
"invalid_auth": "Neveljavna avtentikacija. Preverite dostopni \u017eeton in poskusite znova.",
|
||||||
|
"unknown": "Med nastavitvijo integracije je pri\u0161lo do nepri\u010dakovane napake."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"access_token": "Dostopni \u017eeton za API Vilfo Router",
|
||||||
|
"host": "Ime gostitelja usmerjevalnika ali IP"
|
||||||
|
},
|
||||||
|
"description": "Nastavite integracijo Vilfo Router. Potrebujete ime gostitelja ali IP Vilfo usmerjevalnika in dostopni \u017eeton API. Za dodatne informacije o tej integraciji in kako do teh podrobnosti obi\u0161\u010dite: https://www.home-assistant.io/integrations/vilfo",
|
||||||
|
"title": "Pove\u017eite se z usmerjevalnikom Vilfo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Vilfo Router"
|
||||||
|
}
|
||||||
|
}
|
@@ -6,7 +6,7 @@
|
|||||||
"already_setup_with_diff_host_and_name": "Denne oppf\u00f8ringen ser ut til \u00e5 allerede v\u00e6re konfigurert med en annen vert og navn basert p\u00e5 serienummeret. Fjern den gamle oppf\u00f8ringer fra konfigurasjonen.yaml og fra integrasjonsmenyen f\u00f8r du pr\u00f8ver ut \u00e5 legge til denne enheten p\u00e5 nytt.",
|
"already_setup_with_diff_host_and_name": "Denne oppf\u00f8ringen ser ut til \u00e5 allerede v\u00e6re konfigurert med en annen vert og navn basert p\u00e5 serienummeret. Fjern den gamle oppf\u00f8ringer fra konfigurasjonen.yaml og fra integrasjonsmenyen f\u00f8r du pr\u00f8ver ut \u00e5 legge til denne enheten p\u00e5 nytt.",
|
||||||
"host_exists": "Vizio komponent med vert allerede konfigurert.",
|
"host_exists": "Vizio komponent med vert allerede konfigurert.",
|
||||||
"name_exists": "Vizio-komponent med navn som allerede er konfigurert.",
|
"name_exists": "Vizio-komponent med navn som allerede er konfigurert.",
|
||||||
"updated_entry": "Denne oppf\u00f8ringen er allerede konfigurert, men navnet og / eller alternativene som er definert i konfigurasjonen, stemmer ikke overens med den tidligere importerte konfigurasjonen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.",
|
"updated_entry": "Denne oppf\u00f8ringen er allerede konfigurert, men navnet og / eller alternativene som er definert i konfigurasjonen samsvarer ikke med den tidligere importerte konfigurasjonen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.",
|
||||||
"updated_options": "Denne oppf\u00f8ringen er allerede konfigurert, men alternativene som er definert i konfigurasjonen samsvarer ikke med de tidligere importerte alternativverdiene, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.",
|
"updated_options": "Denne oppf\u00f8ringen er allerede konfigurert, men alternativene som er definert i konfigurasjonen samsvarer ikke med de tidligere importerte alternativverdiene, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.",
|
||||||
"updated_volume_step": "Denne oppf\u00f8ringen er allerede konfigurert, men volumstrinnst\u00f8rrelsen i konfigurasjonen samsvarer ikke med konfigurasjonsoppf\u00f8ringen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter."
|
"updated_volume_step": "Denne oppf\u00f8ringen er allerede konfigurert, men volumstrinnst\u00f8rrelsen i konfigurasjonen samsvarer ikke med konfigurasjonsoppf\u00f8ringen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter."
|
||||||
},
|
},
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
"already_setup_with_diff_host_and_name": "Den h\u00e4r posten verkar redan ha st\u00e4llts in med en annan v\u00e4rd och ett annat namn baserat p\u00e5 dess serienummer. Ta bort alla gamla poster fr\u00e5n configuration.yaml och fr\u00e5n menyn Integrationer innan du f\u00f6rs\u00f6ker l\u00e4gga till den h\u00e4r enheten igen.",
|
"already_setup_with_diff_host_and_name": "Den h\u00e4r posten verkar redan ha st\u00e4llts in med en annan v\u00e4rd och ett annat namn baserat p\u00e5 dess serienummer. Ta bort alla gamla poster fr\u00e5n configuration.yaml och fr\u00e5n menyn Integrationer innan du f\u00f6rs\u00f6ker l\u00e4gga till den h\u00e4r enheten igen.",
|
||||||
"host_exists": "Vizio-komponenten med v\u00e4rdnamnet \u00e4r redan konfigurerad.",
|
"host_exists": "Vizio-komponenten med v\u00e4rdnamnet \u00e4r redan konfigurerad.",
|
||||||
"name_exists": "Vizio-komponent med namn redan konfigurerad.",
|
"name_exists": "Vizio-komponent med namn redan konfigurerad.",
|
||||||
"updated_entry": "Den h\u00e4r posten har redan konfigurerats men namnet och/eller alternativen som definierats i konfigurationen matchar inte den tidigare importerade konfigurationen s\u00e5 konfigureringsposten har uppdaterats i enlighet med detta.",
|
"updated_entry": "Den h\u00e4r posten har redan konfigurerats, men namnet och/eller alternativen som definierats i konfigurationen matchar inte den tidigare importerade konfigurationen och d\u00e4rf\u00f6r har konfigureringsposten uppdaterats i enlighet med detta.",
|
||||||
"updated_options": "Den h\u00e4r posten har redan st\u00e4llts in men de alternativ som definierats i konfigurationen matchar inte de tidigare importerade alternativv\u00e4rdena s\u00e5 konfigurationsposten har uppdaterats i enlighet med detta.",
|
"updated_options": "Den h\u00e4r posten har redan st\u00e4llts in men de alternativ som definierats i konfigurationen matchar inte de tidigare importerade alternativv\u00e4rdena s\u00e5 konfigurationsposten har uppdaterats i enlighet med detta.",
|
||||||
"updated_volume_step": "Den h\u00e4r posten har redan st\u00e4llts in men volymstegstorleken i konfigurationen matchar inte konfigurationsposten s\u00e5 konfigurationsposten har uppdaterats i enlighet med detta."
|
"updated_volume_step": "Den h\u00e4r posten har redan st\u00e4llts in men volymstegstorleken i konfigurationen matchar inte konfigurationsposten s\u00e5 konfigurationsposten har uppdaterats i enlighet med detta."
|
||||||
},
|
},
|
||||||
|
@@ -4,5 +4,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara",
|
"documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara",
|
||||||
"requirements": ["PyXiaomiGateway==0.12.4"],
|
"requirements": ["PyXiaomiGateway==0.12.4"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
|
"after_dependencies": ["discovery"],
|
||||||
"codeowners": ["@danielhiversen", "@syssi"]
|
"codeowners": ["@danielhiversen", "@syssi"]
|
||||||
}
|
}
|
||||||
|
@@ -4,5 +4,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/yeelight",
|
"documentation": "https://www.home-assistant.io/integrations/yeelight",
|
||||||
"requirements": ["yeelight==0.5.0"],
|
"requirements": ["yeelight==0.5.0"],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
|
"after_dependencies": ["discovery"],
|
||||||
"codeowners": ["@rytilahti", "@zewelor"]
|
"codeowners": ["@rytilahti", "@zewelor"]
|
||||||
}
|
}
|
||||||
|
@@ -108,7 +108,6 @@ def setup(hass, config):
|
|||||||
|
|
||||||
def stop_zeroconf(_):
|
def stop_zeroconf(_):
|
||||||
"""Stop Zeroconf."""
|
"""Stop Zeroconf."""
|
||||||
zeroconf.unregister_service(info)
|
|
||||||
zeroconf.close()
|
zeroconf.close()
|
||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf)
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf)
|
||||||
|
@@ -1288,6 +1288,9 @@ class Config:
|
|||||||
# List of allowed external dirs to access
|
# List of allowed external dirs to access
|
||||||
self.whitelist_external_dirs: Set[str] = set()
|
self.whitelist_external_dirs: Set[str] = set()
|
||||||
|
|
||||||
|
# If Home Assistant is running in safe mode
|
||||||
|
self.safe_mode: bool = False
|
||||||
|
|
||||||
def distance(self, lat: float, lon: float) -> Optional[float]:
|
def distance(self, lat: float, lon: float) -> Optional[float]:
|
||||||
"""Calculate distance from Home Assistant.
|
"""Calculate distance from Home Assistant.
|
||||||
|
|
||||||
@@ -1350,6 +1353,7 @@ class Config:
|
|||||||
"whitelist_external_dirs": self.whitelist_external_dirs,
|
"whitelist_external_dirs": self.whitelist_external_dirs,
|
||||||
"version": __version__,
|
"version": __version__,
|
||||||
"config_source": self.config_source,
|
"config_source": self.config_source,
|
||||||
|
"safe_mode": self.safe_mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
def set_time_zone(self, time_zone_str: str) -> None:
|
def set_time_zone(self, time_zone_str: str) -> None:
|
||||||
|
@@ -392,11 +392,14 @@ class EntityRegistry:
|
|||||||
unique_id=entity["unique_id"],
|
unique_id=entity["unique_id"],
|
||||||
platform=entity["platform"],
|
platform=entity["platform"],
|
||||||
name=entity.get("name"),
|
name=entity.get("name"),
|
||||||
|
icon=entity.get("icon"),
|
||||||
disabled_by=entity.get("disabled_by"),
|
disabled_by=entity.get("disabled_by"),
|
||||||
capabilities=entity.get("capabilities") or {},
|
capabilities=entity.get("capabilities") or {},
|
||||||
supported_features=entity.get("supported_features", 0),
|
supported_features=entity.get("supported_features", 0),
|
||||||
device_class=entity.get("device_class"),
|
device_class=entity.get("device_class"),
|
||||||
unit_of_measurement=entity.get("unit_of_measurement"),
|
unit_of_measurement=entity.get("unit_of_measurement"),
|
||||||
|
original_name=entity.get("original_name"),
|
||||||
|
original_icon=entity.get("original_icon"),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.entities = entities
|
self.entities = entities
|
||||||
@@ -419,11 +422,14 @@ class EntityRegistry:
|
|||||||
"unique_id": entry.unique_id,
|
"unique_id": entry.unique_id,
|
||||||
"platform": entry.platform,
|
"platform": entry.platform,
|
||||||
"name": entry.name,
|
"name": entry.name,
|
||||||
|
"icon": entry.icon,
|
||||||
"disabled_by": entry.disabled_by,
|
"disabled_by": entry.disabled_by,
|
||||||
"capabilities": entry.capabilities,
|
"capabilities": entry.capabilities,
|
||||||
"supported_features": entry.supported_features,
|
"supported_features": entry.supported_features,
|
||||||
"device_class": entry.device_class,
|
"device_class": entry.device_class,
|
||||||
"unit_of_measurement": entry.unit_of_measurement,
|
"unit_of_measurement": entry.unit_of_measurement,
|
||||||
|
"original_name": entry.original_name,
|
||||||
|
"original_icon": entry.original_icon,
|
||||||
}
|
}
|
||||||
for entry in self.entities.values()
|
for entry in self.entities.values()
|
||||||
]
|
]
|
||||||
|
@@ -41,7 +41,6 @@ DATA_INTEGRATIONS = "integrations"
|
|||||||
DATA_CUSTOM_COMPONENTS = "custom_components"
|
DATA_CUSTOM_COMPONENTS = "custom_components"
|
||||||
PACKAGE_CUSTOM_COMPONENTS = "custom_components"
|
PACKAGE_CUSTOM_COMPONENTS = "custom_components"
|
||||||
PACKAGE_BUILTIN = "homeassistant.components"
|
PACKAGE_BUILTIN = "homeassistant.components"
|
||||||
LOOKUP_PATHS = [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]
|
|
||||||
CUSTOM_WARNING = (
|
CUSTOM_WARNING = (
|
||||||
"You are using a custom integration for %s which has not "
|
"You are using a custom integration for %s which has not "
|
||||||
"been tested by Home Assistant. This component might "
|
"been tested by Home Assistant. This component might "
|
||||||
@@ -67,6 +66,9 @@ async def _async_get_custom_components(
|
|||||||
hass: "HomeAssistant",
|
hass: "HomeAssistant",
|
||||||
) -> Dict[str, "Integration"]:
|
) -> Dict[str, "Integration"]:
|
||||||
"""Return list of custom integrations."""
|
"""Return list of custom integrations."""
|
||||||
|
if hass.config.safe_mode:
|
||||||
|
return {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import custom_components
|
import custom_components
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -178,7 +180,7 @@ class Integration:
|
|||||||
|
|
||||||
Will create a stub manifest.
|
Will create a stub manifest.
|
||||||
"""
|
"""
|
||||||
comp = _load_file(hass, domain, LOOKUP_PATHS)
|
comp = _load_file(hass, domain, _lookup_path(hass))
|
||||||
|
|
||||||
if comp is None:
|
if comp is None:
|
||||||
return None
|
return None
|
||||||
@@ -464,7 +466,7 @@ class Components:
|
|||||||
component: Optional[ModuleType] = integration.get_component()
|
component: Optional[ModuleType] = integration.get_component()
|
||||||
else:
|
else:
|
||||||
# Fallback to importing old-school
|
# Fallback to importing old-school
|
||||||
component = _load_file(self._hass, comp_name, LOOKUP_PATHS)
|
component = _load_file(self._hass, comp_name, _lookup_path(self._hass))
|
||||||
|
|
||||||
if component is None:
|
if component is None:
|
||||||
raise ImportError(f"Unable to load {comp_name}")
|
raise ImportError(f"Unable to load {comp_name}")
|
||||||
@@ -546,3 +548,10 @@ def _async_mount_config_dir(hass: "HomeAssistant") -> bool:
|
|||||||
if hass.config.config_dir not in sys.path:
|
if hass.config.config_dir not in sys.path:
|
||||||
sys.path.insert(0, hass.config.config_dir)
|
sys.path.insert(0, hass.config.config_dir)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _lookup_path(hass: "HomeAssistant") -> List[str]:
|
||||||
|
"""Return the lookup paths for legacy lookups."""
|
||||||
|
if hass.config.safe_mode:
|
||||||
|
return [PACKAGE_BUILTIN]
|
||||||
|
return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]
|
||||||
|
@@ -11,7 +11,7 @@ cryptography==2.8
|
|||||||
defusedxml==0.6.0
|
defusedxml==0.6.0
|
||||||
distro==1.4.0
|
distro==1.4.0
|
||||||
hass-nabucasa==0.31
|
hass-nabucasa==0.31
|
||||||
home-assistant-frontend==20200217.0
|
home-assistant-frontend==20200219.0
|
||||||
importlib-metadata==1.5.0
|
importlib-metadata==1.5.0
|
||||||
jinja2>=2.10.3
|
jinja2>=2.10.3
|
||||||
netdisco==2.6.0
|
netdisco==2.6.0
|
||||||
|
@@ -4,7 +4,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from typing import Dict, List, Optional, Type, Union
|
from typing import Any, Dict, List, Optional, Type, Union
|
||||||
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ def save_json(
|
|||||||
_LOGGER.error("JSON replacement cleanup failed: %s", err)
|
_LOGGER.error("JSON replacement cleanup failed: %s", err)
|
||||||
|
|
||||||
|
|
||||||
def find_paths_unserializable_data(bad_data: Union[List, Dict]) -> List[str]:
|
def find_paths_unserializable_data(bad_data: Any) -> List[str]:
|
||||||
"""Find the paths to unserializable data.
|
"""Find the paths to unserializable data.
|
||||||
|
|
||||||
This method is slow! Only use for error handling.
|
This method is slow! Only use for error handling.
|
||||||
@@ -98,9 +98,9 @@ def find_paths_unserializable_data(bad_data: Union[List, Dict]) -> List[str]:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
json.dumps(obj)
|
json.dumps(obj)
|
||||||
valid = True
|
continue
|
||||||
except TypeError:
|
except TypeError:
|
||||||
valid = False
|
pass
|
||||||
|
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
for key, value in obj.items():
|
for key, value in obj.items():
|
||||||
@@ -115,7 +115,7 @@ def find_paths_unserializable_data(bad_data: Union[List, Dict]) -> List[str]:
|
|||||||
elif isinstance(obj, list):
|
elif isinstance(obj, list):
|
||||||
for idx, value in enumerate(obj):
|
for idx, value in enumerate(obj):
|
||||||
to_process.append((value, f"{obj_path}[{idx}]"))
|
to_process.append((value, f"{obj_path}[{idx}]"))
|
||||||
elif not valid: # type: ignore
|
else:
|
||||||
invalid.append(obj_path)
|
invalid.append(obj_path)
|
||||||
|
|
||||||
return invalid
|
return invalid
|
||||||
|
@@ -80,6 +80,7 @@ class AsyncHandler:
|
|||||||
|
|
||||||
def _process(self) -> None:
|
def _process(self) -> None:
|
||||||
"""Process log in a thread."""
|
"""Process log in a thread."""
|
||||||
|
try:
|
||||||
while True:
|
while True:
|
||||||
record = asyncio.run_coroutine_threadsafe(
|
record = asyncio.run_coroutine_threadsafe(
|
||||||
self._queue.get(), self.loop
|
self._queue.get(), self.loop
|
||||||
@@ -90,6 +91,8 @@ class AsyncHandler:
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.handler.emit(record)
|
self.handler.emit(record)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
self.handler.close()
|
||||||
|
|
||||||
def createLock(self) -> None:
|
def createLock(self) -> None:
|
||||||
"""Ignore lock stuff."""
|
"""Ignore lock stuff."""
|
||||||
|
@@ -123,13 +123,13 @@ adguardhome==0.4.1
|
|||||||
afsapi==0.0.4
|
afsapi==0.0.4
|
||||||
|
|
||||||
# homeassistant.components.geonetnz_quakes
|
# homeassistant.components.geonetnz_quakes
|
||||||
aio_geojson_geonetnz_quakes==0.11
|
aio_geojson_geonetnz_quakes==0.12
|
||||||
|
|
||||||
# homeassistant.components.geonetnz_volcano
|
# homeassistant.components.geonetnz_volcano
|
||||||
aio_geojson_geonetnz_volcano==0.5
|
aio_geojson_geonetnz_volcano==0.5
|
||||||
|
|
||||||
# homeassistant.components.nsw_rural_fire_service_feed
|
# homeassistant.components.nsw_rural_fire_service_feed
|
||||||
aio_geojson_nsw_rfs_incidents==0.1
|
aio_geojson_nsw_rfs_incidents==0.3
|
||||||
|
|
||||||
# homeassistant.components.gdacs
|
# homeassistant.components.gdacs
|
||||||
aio_georss_gdacs==0.3
|
aio_georss_gdacs==0.3
|
||||||
@@ -199,7 +199,7 @@ aiopylgtv==0.3.3
|
|||||||
aioswitcher==2019.4.26
|
aioswitcher==2019.4.26
|
||||||
|
|
||||||
# homeassistant.components.unifi
|
# homeassistant.components.unifi
|
||||||
aiounifi==12
|
aiounifi==13
|
||||||
|
|
||||||
# homeassistant.components.wwlln
|
# homeassistant.components.wwlln
|
||||||
aiowwlln==2.0.2
|
aiowwlln==2.0.2
|
||||||
@@ -305,7 +305,7 @@ beewi_smartclim==0.0.7
|
|||||||
bellows-homeassistant==0.13.2
|
bellows-homeassistant==0.13.2
|
||||||
|
|
||||||
# homeassistant.components.bmw_connected_drive
|
# homeassistant.components.bmw_connected_drive
|
||||||
bimmer_connected==0.7.0
|
bimmer_connected==0.7.1
|
||||||
|
|
||||||
# homeassistant.components.bizkaibus
|
# homeassistant.components.bizkaibus
|
||||||
bizkaibus==0.1.1
|
bizkaibus==0.1.1
|
||||||
@@ -683,7 +683,7 @@ hole==0.5.0
|
|||||||
holidays==0.10.1
|
holidays==0.10.1
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20200217.0
|
home-assistant-frontend==20200219.0
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.8
|
homeassistant-pyozw==0.1.8
|
||||||
@@ -1072,7 +1072,7 @@ pushetta==1.0.15
|
|||||||
pushover_complete==1.1.1
|
pushover_complete==1.1.1
|
||||||
|
|
||||||
# homeassistant.components.rpi_gpio_pwm
|
# homeassistant.components.rpi_gpio_pwm
|
||||||
pwmled==1.4.1
|
pwmled==1.5.0
|
||||||
|
|
||||||
# homeassistant.components.august
|
# homeassistant.components.august
|
||||||
py-august==0.14.0
|
py-august==0.14.0
|
||||||
@@ -1460,7 +1460,7 @@ pypjlink2==1.2.0
|
|||||||
pypoint==1.1.2
|
pypoint==1.1.2
|
||||||
|
|
||||||
# homeassistant.components.ps4
|
# homeassistant.components.ps4
|
||||||
pyps4-2ndscreen==1.0.6
|
pyps4-2ndscreen==1.0.7
|
||||||
|
|
||||||
# homeassistant.components.qwikswitch
|
# homeassistant.components.qwikswitch
|
||||||
pyqwikswitch==0.93
|
pyqwikswitch==0.93
|
||||||
@@ -2025,7 +2025,7 @@ uscisstatus==0.1.1
|
|||||||
uvcclient==0.11.0
|
uvcclient==0.11.0
|
||||||
|
|
||||||
# homeassistant.components.vallox
|
# homeassistant.components.vallox
|
||||||
vallox-websocket-api==2.2.0
|
vallox-websocket-api==2.4.0
|
||||||
|
|
||||||
# homeassistant.components.venstar
|
# homeassistant.components.venstar
|
||||||
venstarcolortouch==0.12
|
venstarcolortouch==0.12
|
||||||
|
@@ -7,7 +7,7 @@ asynctest==0.13.0
|
|||||||
codecov==2.0.15
|
codecov==2.0.15
|
||||||
mock-open==1.3.1
|
mock-open==1.3.1
|
||||||
mypy==0.761
|
mypy==0.761
|
||||||
pre-commit==2.0.1
|
pre-commit==2.1.0
|
||||||
pylint==2.4.4
|
pylint==2.4.4
|
||||||
astroid==2.3.3
|
astroid==2.3.3
|
||||||
pylint-strict-informational==0.1
|
pylint-strict-informational==0.1
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user