Merge branch 'dev' into rc

This commit is contained in:
Franck Nijhof
2020-02-19 13:22:32 +01:00
133 changed files with 3350 additions and 488 deletions

View File

@@ -756,7 +756,6 @@ omit =
homeassistant/components/twentemilieu/sensor.py
homeassistant/components/twilio_call/notify.py
homeassistant/components/twilio_sms/notify.py
homeassistant/components/twitch/sensor.py
homeassistant/components/twitter/notify.py
homeassistant/components/ubee/device_tracker.py
homeassistant/components/ubus/device_tracker.py

View File

@@ -1,5 +1,6 @@
"""Provide methods to bootstrap a Home Assistant instance."""
import asyncio
import contextlib
import logging
import logging.handlers
import os
@@ -7,12 +8,14 @@ import sys
from time import monotonic
from typing import Any, Dict, Optional, Set
from async_timeout import timeout
import voluptuous as vol
from homeassistant import config as conf_util, config_entries, core, loader
from homeassistant.components import http
from homeassistant.const import (
EVENT_HOMEASSISTANT_CLOSE,
EVENT_HOMEASSISTANT_STOP,
REQUIRED_NEXT_PYTHON_DATE,
REQUIRED_NEXT_PYTHON_VER,
)
@@ -80,8 +83,7 @@ async def async_setup_hass(
config_dict = await conf_util.async_hass_config_yaml(hass)
except HomeAssistantError as err:
_LOGGER.error(
"Failed to parse configuration.yaml: %s. Falling back to safe mode",
err,
"Failed to parse configuration.yaml: %s. Activating safe mode", err,
)
else:
if not is_virtual_env():
@@ -93,8 +95,30 @@ async def async_setup_hass(
finally:
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")
hass.config.safe_mode = True
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)
# Add config entry domains
if "safe_mode" not in config:
if not hass.config.safe_mode:
domains.update(hass.config_entries.async_domains())
# Make sure the Hass.io component is loaded

View File

@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"requirements": ["pyatv==0.3.13"],
"dependencies": ["configurator"],
"after_dependencies": ["discovery"],
"codeowners": []
}

View File

@@ -225,6 +225,13 @@ class AugustData:
self._door_state_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
def house_ids(self):
"""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
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):
"""Return status if the door is locked or unlocked.
@@ -497,6 +512,33 @@ class AugustData:
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(
device_name, operation_name, func, *args, **kwargs

View File

@@ -12,7 +12,7 @@ from . import DATA_AUGUST
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
SCAN_INTERVAL = timedelta(seconds=5)
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:
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 None
SENSOR_NAME = 0
SENSOR_DEVICE_CLASS = 1
SENSOR_STATE_PROVIDER = 2
# sensor_type: [name, device_class, async_state_provider]
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 sensor_type in SENSOR_TYPES_DOOR:
async_state_provider = SENSOR_TYPES_DOOR[sensor_type][2]
if await async_state_provider(data, door) is LockDoorStatus.UNKNOWN:
if not data.lock_has_doorsense(door.device_id):
_LOGGER.debug(
"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,
)
continue
_LOGGER.debug(
"Adding sensor class %s for %s",
SENSOR_TYPES_DOOR[sensor_type][1],
SENSOR_TYPES_DOOR[sensor_type][SENSOR_DEVICE_CLASS],
door.device_name,
)
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:
_LOGGER.debug(
"Adding doorbell sensor class %s for %s",
SENSOR_TYPES_DOORBELL[sensor_type][1],
SENSOR_TYPES_DOORBELL[sensor_type][SENSOR_DEVICE_CLASS],
doorbell.device_name,
)
devices.append(AugustDoorbellBinarySensor(data, sensor_type, doorbell))
@@ -125,22 +128,25 @@ class AugustDoorBinarySensor(BinarySensorDevice):
@property
def device_class(self):
"""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
def name(self):
"""Return the name of the binary sensor."""
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):
"""Get the latest state of the sensor and update activity."""
async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2]
self._state = await async_state_provider(self._data, self._door)
self._available = self._state is not None
self._state = self._state == LockDoorStatus.OPEN
async_state_provider = SENSOR_TYPES_DOOR[self._sensor_type][
SENSOR_STATE_PROVIDER
]
lock_door_state = await async_state_provider(self._data, self._door)
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(
self._door.device_id, ActivityType.DOOR_OPERATION
@@ -193,7 +199,8 @@ class AugustDoorBinarySensor(BinarySensorDevice):
def unique_id(self) -> str:
"""Get the unique of the door open binary sensor."""
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
def device_class(self):
"""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
def name(self):
"""Return the name of the binary sensor."""
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):
"""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._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
def unique_id(self) -> str:
"""Get the unique id of the doorbell sensor."""
return "{:s}_{:s}".format(
self._doorbell.device_id,
SENSOR_TYPES_DOORBELL[self._sensor_type][0].lower(),
SENSOR_TYPES_DOORBELL[self._sensor_type][SENSOR_NAME].lower(),
)

View File

@@ -67,7 +67,9 @@ class AugustLock(LockDevice):
async def async_update(self):
"""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._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)
lock_activity = await self._data.async_get_latest_device_activity(

View File

@@ -11,8 +11,10 @@ import homeassistant.helpers.config_validation as cv
# mypy: allow-untyped-defs
CONF_ENCODING = "encoding"
CONF_QOS = "qos"
CONF_TOPIC = "topic"
DEFAULT_ENCODING = "utf-8"
DEFAULT_QOS = 0
TRIGGER_SCHEMA = vol.Schema(
{
@@ -20,6 +22,9 @@ TRIGGER_SCHEMA = vol.Schema(
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD): 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]
payload = config.get(CONF_PAYLOAD)
encoding = config[CONF_ENCODING] or None
qos = config[CONF_QOS]
@callback
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})
remove = await mqtt.async_subscribe(
hass, topic, mqtt_automation_listener, encoding=encoding
hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos
)
return remove

View File

@@ -2,7 +2,7 @@
"config": {
"abort": {
"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",
"not_axis_device": "Oppdaget enhet ikke en Axis enhet",
"updated_configuration": "Oppdatert enhetskonfigurasjonen med ny vertsadresse"

View File

@@ -2,7 +2,7 @@
"config": {
"abort": {
"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",
"not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet",
"updated_configuration": "Uppdaterad enhetskonfiguration med ny v\u00e4rdadress"

View File

@@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "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": [],
"codeowners": ["@gerard33"]
}

View File

@@ -108,7 +108,8 @@
"allow_clip_sensor": "Allow deCONZ CLIP sensors",
"allow_deconz_groups": "Allow deCONZ light groups"
},
"description": "Configure visibility of deCONZ device types"
"description": "Configure visibility of deCONZ device types",
"title": "deCONZ options"
}
}
}

View File

@@ -37,6 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.option_allow_clip_sensor
or not sensor.type.startswith("CLIP")
)
and sensor.deconz_id not in gateway.deconz_ids.values()
):
entities.append(DeconzBinarySensor(sensor, gateway))

View File

@@ -44,6 +44,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.option_allow_clip_sensor
or not sensor.type.startswith("CLIP")
)
and sensor.deconz_id not in gateway.deconz_ids.values()
):
entities.append(DeconzThermostat(sensor, gateway))

View File

@@ -77,6 +77,11 @@ class DeconzDevice(DeconzBase, Entity):
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:
"""Disconnect device object when removed."""
@@ -85,6 +90,15 @@ class DeconzDevice(DeconzBase, Entity):
for unsub_dispatcher in self.listeners:
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
def async_update_callback(self, force_update=False, ignore_update=False):
"""Update the device's state."""

View File

@@ -20,6 +20,8 @@ from .const import (
DOMAIN,
LOGGER,
NEW_DEVICE,
NEW_GROUP,
NEW_SENSOR,
SUPPORTED_PLATFORMS,
)
from .errors import AuthenticationRequired, CannotConnect
@@ -45,6 +47,9 @@ class DeconzGateway:
self.events = []
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
def bridgeid(self) -> str:
"""Return the unique identifier of the gateway."""
@@ -108,22 +113,64 @@ class DeconzGateway:
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
@staticmethod
async def async_new_address(hass, entry) -> None:
"""Handle signals of gateway getting new address.
async def async_config_entry_updated(hass, entry) -> None:
"""Handle signals of config entry being updated.
This is a static method because a class method (bound method),
can not be used with weak references.
This is a static method because a class method (bound method), 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)
if gateway.api.host != entry.data[CONF_HOST]:
gateway.api.close()
gateway.api.host = entry.data[CONF_HOST]
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
def signal_reachable(self) -> str:
@@ -141,6 +188,11 @@ class DeconzGateway:
"""Gateway specific event to signal new device."""
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
def async_add_device_callback(self, device_type, device) -> None:
"""Handle event of new device creation in deCONZ."""

View File

@@ -67,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities = []
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))
async_add_entities(entities, True)

View File

@@ -68,6 +68,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.option_allow_clip_sensor
or not sensor.type.startswith("CLIP")
)
and sensor.deconz_id not in gateway.deconz_ids.values()
):
entities.append(DeconzSensor(sensor, gateway))

View File

@@ -14,20 +14,9 @@
"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"
},
"options": {
"title": "Extra configuration options for deCONZ",
"data": {
"allow_clip_sensor": "Allow importing virtual sensors",
"allow_deconz_groups": "Allow importing deCONZ groups"
}
},
"hassio_confirm": {
"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}?",
"data": {
"allow_clip_sensor": "Allow importing virtual sensors",
"allow_deconz_groups": "Allow importing deCONZ groups"
}
"description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?"
}
},
"error": {
@@ -45,11 +34,12 @@
"options": {
"step": {
"deconz_devices": {
"description": "Configure visibility of deCONZ device types",
"data": {
"allow_clip_sensor": "Allow deCONZ CLIP sensors",
"allow_deconz_groups": "Allow deCONZ light groups"
}
},
"description": "Configure visibility of deCONZ device types",
"title": "deCONZ options"
}
}
},
@@ -105,4 +95,4 @@
"side_6": "Side 6"
}
}
}
}

View File

@@ -1,5 +1,22 @@
{
"config": {
"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)"
}
}
}
}
}

View File

@@ -1,5 +1,22 @@
{
"config": {
"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"
}
}
}
}
}

View File

@@ -1,5 +1,28 @@
{
"config": {
"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"
}
}
}
}
}

View File

@@ -1,5 +1,28 @@
{
"config": {
"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"
}
}
}
}
}

View File

@@ -1,5 +1,30 @@
{
"config": {
"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"
}
}
}
}
}

View File

@@ -1,5 +1,28 @@
{
"config": {
"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"
}
}
}
}
}

View File

@@ -1,5 +1,22 @@
{
"config": {
"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"
}
}
}
}
}

View File

@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/freebox",
"requirements": ["aiofreepybox==0.0.8"],
"dependencies": [],
"after_dependencies": ["discovery"],
"codeowners": ["@snoof85"]
}

View File

@@ -508,6 +508,23 @@ def websocket_get_themes(hass, connection, msg):
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(
websocket_api.result_message(
msg["id"],

View File

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

View File

@@ -3,7 +3,7 @@
"name": "GeoNet NZ Quakes",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes",
"requirements": ["aio_geojson_geonetnz_quakes==0.11"],
"requirements": ["aio_geojson_geonetnz_quakes==0.12"],
"dependencies": [],
"codeowners": ["@exxamalte"]
}

View File

@@ -6,7 +6,7 @@
"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.",
"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"
},
"error": {

View File

@@ -7,6 +7,7 @@ from homematicip.aio.device import (
AsyncFullFlushShutter,
AsyncGarageDoorModuleTormatic,
)
from homematicip.aio.group import AsyncExtendedLinkedShutterGroup
from homematicip.base.enums import DoorCommand, DoorState
from homeassistant.components.cover import (
@@ -18,6 +19,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice
from .hap import HomematicipHAP
_LOGGER = logging.getLogger(__name__)
@@ -41,6 +43,10 @@ async def async_setup_entry(
elif isinstance(device, AsyncGarageDoorModuleTormatic):
entities.append(HomematicipGarageDoorModuleTormatic(hap, device))
for group in hap.home.groups:
if isinstance(group, AsyncExtendedLinkedShutterGroup):
entities.append(HomematicipCoverShutterGroup(hap, group))
if entities:
async_add_entities(entities)
@@ -142,3 +148,12 @@ class HomematicipGarageDoorModuleTormatic(HomematicipGenericDevice, CoverDevice)
async def async_stop_cover(self, **kwargs) -> None:
"""Stop the cover."""
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)

View File

@@ -14,6 +14,7 @@ from homematicip.aio.device import (
AsyncPassageDetector,
AsyncPlugableSwitchMeasuring,
AsyncPresenceDetectorIndoor,
AsyncRoomControlDeviceAnalog,
AsyncTemperatureHumiditySensorDisplay,
AsyncTemperatureHumiditySensorOutdoor,
AsyncTemperatureHumiditySensorWithoutDisplay,
@@ -79,6 +80,8 @@ async def async_setup_entry(
):
entities.append(HomematicipTemperatureSensor(hap, device))
entities.append(HomematicipHumiditySensor(hap, device))
elif isinstance(device, (AsyncRoomControlDeviceAnalog,)):
entities.append(HomematicipTemperatureSensor(hap, device))
if isinstance(
device,
(

View File

@@ -8,6 +8,7 @@
"data": {
"latitude": "Breedegrad",
"longitude": "L\u00e4ngegrad",
"mode": "Modus",
"name": "Numm"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",

View File

@@ -8,6 +8,7 @@
"data": {
"latitude": "Latitude",
"longitude": "Longitude",
"mode": "Mode",
"name": "Naam"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",

View File

@@ -8,6 +8,7 @@
"data": {
"latitude": "Zemljepisna \u0161irina",
"longitude": "Zemljepisna dol\u017eina",
"mode": "Na\u010din",
"name": "Ime"
},
"description": "Instituto Portugu\u00eas do Mar e Atmosfera",

View File

@@ -8,6 +8,7 @@
"data": {
"latitude": "Latitud",
"longitude": "Longitud",
"mode": "L\u00e4ge",
"name": "Namn"
},
"description": "Portugisiska institutet f\u00f6r hav och atmosf\u00e4ren",

View File

@@ -29,6 +29,10 @@
"abort": {
"not_konn_panel": "Non \u00e8 un dispositivo Konnected.io riconosciuto"
},
"error": {
"one": "uno",
"other": "altro"
},
"step": {
"options_binary": {
"data": {

View File

@@ -10,7 +10,16 @@
"cannot_connect": "Kann sech net mam Konnected Panel um {host}:{port} verbannen"
},
"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": {
"data": {
"host": "Konnected Apparat IP Adress",
"port": "Konnected Apparat Port"
},
"description": "Informatioune vum Konnected Panel aginn.",
"title": "Konnected Apparat entdecken"
}
},
@@ -20,6 +29,10 @@
"abort": {
"not_konn_panel": "Keen erkannten Konnected.io Apparat"
},
"error": {
"one": "Ee",
"other": "M\u00e9i"
},
"step": {
"options_binary": {
"data": {
@@ -27,13 +40,16 @@
"name": "Numm (optional)",
"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"
},
"options_digital": {
"data": {
"name": "Numm (optional)",
"poll_interval": "Intervall vun den Offroen (Minutten) (optional)",
"type": "Typ vum Sensor"
},
"description": "Wiel d'Optioune fir den digitale Sensor dee mat {zone} verbonnen ass",
"title": "Digitale Sensor konfigur\u00e9ieren"
},
"options_io": {
@@ -47,6 +63,7 @@
"7": "Zon 7",
"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"
},
"options_io_ext": {
@@ -59,14 +76,26 @@
"alarm1": "ALARM1",
"alarm2_out2": "OUT2/ALARM2",
"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": {
"data": {
"activation": "Ausgang wann un",
"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"
}
},

View File

@@ -1,14 +1,28 @@
{
"config": {
"abort": {
"already_configured": "Apparaat is al geconfigureerd",
"unknown": "Onbekende fout opgetreden"
},
"step": {
"confirm": {
"title": "Konnected Apparaat Klaar"
},
"user": {
"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": {
"abort": {
"not_konn_panel": "Geen herkend Konnected.io apparaat"
},
"error": {
"one": "Leeg",
"other": "Leeg"
@@ -16,8 +30,11 @@
"step": {
"options_binary": {
"data": {
"name": "Naam (optioneel)"
}
"inverse": "Keer de open / dicht status om",
"name": "Naam (optioneel)",
"type": "Type binaire sensor"
},
"title": "Binaire sensor configureren"
},
"options_digital": {
"data": {
@@ -48,8 +65,10 @@
"options_switch": {
"data": {
"name": "Naam (optioneel)"
}
},
"title": "Schakelbare uitgang configureren"
}
}
},
"title": "Konnected Alarm Paneel Opties"
}
}

View File

@@ -2,8 +2,27 @@
"config": {
"abort": {
"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."
},
"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"
},
"options": {

View File

@@ -31,7 +31,7 @@
},
"error": {
"one": "Tom",
"other": "Tom"
"other": "Tomma"
},
"step": {
"options_binary": {

View File

@@ -89,6 +89,9 @@ class LovelaceStorage:
async def async_load(self, force):
"""Load config."""
if self._hass.config.safe_mode:
raise ConfigNotFound
if self._data is None:
await self._load()

View File

@@ -1,5 +1,8 @@
{
"config": {
"abort": {
"already_configured": "MELCloud Integratioun ass scho konfigur\u00e9iert fir d\u00ebs Email. Acc\u00e8s Jeton gouf erneiert."
},
"error": {
"cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.",
"invalid_auth": "Ong\u00eblteg Authentifikatioun",
@@ -8,8 +11,10 @@
"step": {
"user": {
"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"
}
},

View 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"
}
}

View File

@@ -8,6 +8,16 @@
"invalid_auth": "Niepoprawne uwierzytelnienie.",
"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"
}
}

View File

@@ -3,13 +3,20 @@
"abort": {
"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": {
"user": {
"data": {
"host": "Apparat",
"name": "Numm",
"port": "Port"
}
},
"description": "Riicht deng Minecraft Server Instanz a fir d'Iwwerwaachung z'erlaben",
"title": "Verbann d\u00e4in Minecraft Server"
}
},
"title": "Minecraft Server"

View File

@@ -4,7 +4,9 @@
"already_configured": "Host is al geconfigureerd."
},
"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": {
"user": {
@@ -12,8 +14,11 @@
"host": "Host",
"name": "Naam",
"port": "Poort"
}
},
"description": "Stel uw Minecraft server in om monitoring toe te staan.",
"title": "Koppel uw Minecraft server"
}
}
},
"title": "Minecraft server"
}
}

View File

@@ -159,10 +159,10 @@ def setup(hass, config):
def write_register(service):
"""Write Modbus registers."""
unit = int(float(service.data.get(ATTR_UNIT)))
address = int(float(service.data.get(ATTR_ADDRESS)))
value = service.data.get(ATTR_VALUE)
client_name = service.data.get(ATTR_HUB)
unit = int(float(service.data[ATTR_UNIT]))
address = int(float(service.data[ATTR_ADDRESS]))
value = service.data[ATTR_VALUE]
client_name = service.data[ATTR_HUB]
if isinstance(value, list):
hub_collect[client_name].write_registers(
unit, address, [int(float(i)) for i in value]
@@ -172,10 +172,10 @@ def setup(hass, config):
def write_coil(service):
"""Write Modbus coil."""
unit = service.data.get(ATTR_UNIT)
address = service.data.get(ATTR_ADDRESS)
state = service.data.get(ATTR_STATE)
client_name = service.data.get(ATTR_HUB)
unit = service.data[ATTR_UNIT]
address = service.data[ATTR_ADDRESS]
state = service.data[ATTR_STATE]
client_name = service.data[ATTR_HUB]
hub_collect[client_name].write_coil(unit, address, state)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)

View File

@@ -25,8 +25,8 @@ CONF_INPUTS = "inputs"
CONF_INPUT_TYPE = "input_type"
CONF_ADDRESS = "address"
INPUT_TYPE_COIL = "coil"
INPUT_TYPE_DISCRETE = "discrete_input"
DEFAULT_INPUT_TYPE_COIL = "coil"
DEFAULT_INPUT_TYPE_DISCRETE = "discrete_input"
PLATFORM_SCHEMA = vol.All(
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_SLAVE): cv.positive_int,
vol.Optional(
CONF_INPUT_TYPE, default=INPUT_TYPE_COIL
): vol.In([INPUT_TYPE_COIL, INPUT_TYPE_DISCRETE]),
CONF_INPUT_TYPE, default=DEFAULT_INPUT_TYPE_COIL
): 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):
"""Set up the Modbus binary sensors."""
sensors = []
for entry in config.get(CONF_INPUTS):
hub = hass.data[MODBUS_DOMAIN][entry.get(CONF_HUB)]
for entry in config[CONF_INPUTS]:
hub = hass.data[MODBUS_DOMAIN][entry[CONF_HUB]]
sensors.append(
ModbusBinarySensor(
hub,
entry.get(CONF_NAME),
entry[CONF_NAME],
entry.get(CONF_SLAVE),
entry.get(CONF_ADDRESS),
entry[CONF_ADDRESS],
entry.get(CONF_DEVICE_CLASS),
entry.get(CONF_INPUT_TYPE),
entry[CONF_INPUT_TYPE],
)
)
@@ -110,7 +112,7 @@ class ModbusBinarySensor(BinarySensorDevice):
def update(self):
"""Update the state of the sensor."""
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)
else:
result = self._hub.read_discrete_inputs(self._slave, self._address, 1)

View File

@@ -27,6 +27,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_TARGET_TEMP = "target_temp_register"
CONF_CURRENT_TEMP = "current_temp_register"
CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type"
CONF_DATA_TYPE = "data_type"
CONF_COUNT = "data_count"
CONF_PRECISION = "precision"
@@ -42,6 +43,9 @@ CONF_STEP = "temp_step"
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
HVAC_MODES = [HVAC_MODE_AUTO]
DEFAULT_REGISTER_TYPE_HOLDING = "holding"
DEFAULT_REGISTER_TYPE_INPUT = "input"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
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_TARGET_TEMP): 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(
[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):
"""Set up the Modbus Thermostat Platform."""
name = config.get(CONF_NAME)
modbus_slave = config.get(CONF_SLAVE)
target_temp_register = config.get(CONF_TARGET_TEMP)
current_temp_register = config.get(CONF_CURRENT_TEMP)
data_type = config.get(CONF_DATA_TYPE)
count = config.get(CONF_COUNT)
precision = config.get(CONF_PRECISION)
scale = config.get(CONF_SCALE)
offset = config.get(CONF_OFFSET)
unit = config.get(CONF_UNIT)
max_temp = config.get(CONF_MAX_TEMP)
min_temp = config.get(CONF_MIN_TEMP)
temp_step = config.get(CONF_STEP)
hub_name = config.get(CONF_HUB)
name = config[CONF_NAME]
modbus_slave = config[CONF_SLAVE]
target_temp_register = config[CONF_TARGET_TEMP]
current_temp_register = config[CONF_CURRENT_TEMP]
current_temp_register_type = config[CONF_CURRENT_TEMP_REGISTER_TYPE]
data_type = config[CONF_DATA_TYPE]
count = config[CONF_COUNT]
precision = config[CONF_PRECISION]
scale = config[CONF_SCALE]
offset = config[CONF_OFFSET]
unit = config[CONF_UNIT]
max_temp = config[CONF_MAX_TEMP]
min_temp = config[CONF_MIN_TEMP]
temp_step = config[CONF_STEP]
hub_name = config[CONF_HUB]
hub = hass.data[MODBUS_DOMAIN][hub_name]
add_entities(
@@ -90,6 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
modbus_slave,
target_temp_register,
current_temp_register,
current_temp_register_type,
data_type,
count,
precision,
@@ -115,6 +124,7 @@ class ModbusThermostat(ClimateDevice):
modbus_slave,
target_temp_register,
current_temp_register,
current_temp_register_type,
data_type,
count,
precision,
@@ -131,6 +141,7 @@ class ModbusThermostat(ClimateDevice):
self._slave = modbus_slave
self._target_temperature_register = target_temp_register
self._current_temperature_register = current_temp_register
self._current_temperature_register_type = current_temp_register_type
self._target_temperature = None
self._current_temperature = None
self._data_type = data_type
@@ -161,10 +172,10 @@ class ModbusThermostat(ClimateDevice):
def update(self):
"""Update Target & Current Temperature."""
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_register
self._current_temperature_register_type, self._current_temperature_register
)
@property
@@ -228,12 +239,17 @@ class ModbusThermostat(ClimateDevice):
"""Return True if entity is available."""
return self._available
def _read_register(self, register) -> Optional[float]:
"""Read holding register using the Modbus hub slave."""
def _read_register(self, register_type, register) -> Optional[float]:
"""Read register using the Modbus hub slave."""
try:
result = self._hub.read_holding_registers(
self._slave, register, self._count
)
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(
self._slave, register, self._count
)
except ConnectionException:
self._set_unavailable(register)
return

View File

@@ -37,8 +37,8 @@ DATA_TYPE_FLOAT = "float"
DATA_TYPE_INT = "int"
DATA_TYPE_UINT = "uint"
REGISTER_TYPE_HOLDING = "holding"
REGISTER_TYPE_INPUT = "input"
DEFAULT_REGISTER_TYPE_HOLDING = "holding"
DEFAULT_REGISTER_TYPE_INPUT = "input"
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_OFFSET, default=0): number,
vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): vol.In(
[REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]
),
vol.Optional(
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_SCALE, default=1): number,
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_FLOAT] = {1: "e", 2: "f", 4: "d"}
for register in config.get(CONF_REGISTERS):
for register in config[CONF_REGISTERS]:
structure = ">i"
if register.get(CONF_DATA_TYPE) != DATA_TYPE_CUSTOM:
if register[CONF_DATA_TYPE] != DATA_TYPE_CUSTOM:
try:
structure = ">{}".format(
data_types[register.get(CONF_DATA_TYPE)][register.get(CONF_COUNT)]
data_types[register[CONF_DATA_TYPE]][register[CONF_COUNT]]
)
except KeyError:
_LOGGER.error(
"Unable to detect data type for %s sensor, try a custom type",
register.get(CONF_NAME),
register[CONF_NAME],
)
continue
else:
@@ -114,35 +114,33 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
try:
size = struct.calcsize(structure)
except struct.error as err:
_LOGGER.error(
"Error in sensor %s structure: %s", register.get(CONF_NAME), err
)
_LOGGER.error("Error in sensor %s structure: %s", register[CONF_NAME], err)
continue
if register.get(CONF_COUNT) * 2 != size:
if register[CONF_COUNT] * 2 != size:
_LOGGER.error(
"Structure size (%d bytes) mismatch registers count (%d words)",
size,
register.get(CONF_COUNT),
register[CONF_COUNT],
)
continue
hub_name = register.get(CONF_HUB)
hub_name = register[CONF_HUB]
hub = hass.data[MODBUS_DOMAIN][hub_name]
sensors.append(
ModbusRegisterSensor(
hub,
register.get(CONF_NAME),
register[CONF_NAME],
register.get(CONF_SLAVE),
register.get(CONF_REGISTER),
register.get(CONF_REGISTER_TYPE),
register[CONF_REGISTER],
register[CONF_REGISTER_TYPE],
register.get(CONF_UNIT_OF_MEASUREMENT),
register.get(CONF_COUNT),
register.get(CONF_REVERSE_ORDER),
register.get(CONF_SCALE),
register.get(CONF_OFFSET),
register[CONF_COUNT],
register[CONF_REVERSE_ORDER],
register[CONF_SCALE],
register[CONF_OFFSET],
structure,
register.get(CONF_PRECISION),
register[CONF_PRECISION],
register.get(CONF_DEVICE_CLASS),
)
)
@@ -223,7 +221,7 @@ class ModbusRegisterSensor(RestoreEntity):
def update(self):
"""Update the state of the sensor."""
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, self._count
)

View File

@@ -32,8 +32,8 @@ CONF_STATE_ON = "state_on"
CONF_VERIFY_REGISTER = "verify_register"
CONF_VERIFY_STATE = "verify_state"
REGISTER_TYPE_HOLDING = "holding"
REGISTER_TYPE_INPUT = "input"
DEFAULT_REGISTER_TYPE_HOLDING = "holding"
DEFAULT_REGISTER_TYPE_INPUT = "input"
REGISTERS_SCHEMA = vol.Schema(
{
@@ -42,8 +42,8 @@ REGISTERS_SCHEMA = vol.Schema(
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_REGISTER): cv.positive_int,
vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string,
vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): vol.In(
[REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]
vol.Optional(CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING): vol.In(
[DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]
),
vol.Optional(CONF_SLAVE): 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."""
switches = []
if CONF_COILS in config:
for coil in config.get(CONF_COILS):
hub_name = coil.get(CONF_HUB)
for coil in config[CONF_COILS]:
hub_name = coil[CONF_HUB]
hub = hass.data[MODBUS_DOMAIN][hub_name]
switches.append(
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:
for register in config.get(CONF_REGISTERS):
hub_name = register.get(CONF_HUB)
for register in config[CONF_REGISTERS]:
hub_name = register[CONF_HUB]
hub = hass.data[MODBUS_DOMAIN][hub_name]
switches.append(
ModbusRegisterSwitch(
hub,
register.get(CONF_NAME),
register[CONF_NAME],
register.get(CONF_SLAVE),
register.get(CONF_REGISTER),
register.get(CONF_COMMAND_ON),
register.get(CONF_COMMAND_OFF),
register.get(CONF_VERIFY_STATE),
register[CONF_REGISTER],
register[CONF_COMMAND_ON],
register[CONF_COMMAND_OFF],
register[CONF_VERIFY_STATE],
register.get(CONF_VERIFY_REGISTER),
register.get(CONF_REGISTER_TYPE),
register[CONF_REGISTER_TYPE],
register.get(CONF_STATE_ON),
register.get(CONF_STATE_OFF),
)
@@ -242,7 +242,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
"""Set switch on."""
# 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)
if not self._verify_state:
self._is_on = True
@@ -251,7 +251,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
"""Set switch off."""
# 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)
if not self._verify_state:
self._is_on = False
@@ -282,7 +282,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
def _read_register(self) -> Optional[int]:
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)
else:
result = self._hub.read_holding_registers(

View File

@@ -27,5 +27,27 @@
}
},
"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"
}
}
}

View File

@@ -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):
"""Mixin used for mqtt platforms that support the device registry."""
@@ -1216,32 +1244,7 @@ class MqttEntityDeviceInfo(Entity):
@property
def device_info(self):
"""Return a device description for device registry."""
if not 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
return device_info_from_config(self._device_config)
@websocket_api.async_response

View File

@@ -3,6 +3,7 @@
ABBREVIATIONS = {
"act_t": "action_topic",
"act_tpl": "action_template",
"atype": "automation_type",
"aux_cmd_t": "aux_command_topic",
"aux_stat_tpl": "aux_state_template",
"aux_stat_t": "aux_state_topic",
@@ -80,6 +81,7 @@ ABBREVIATIONS = {
"osc_cmd_t": "oscillation_command_topic",
"osc_stat_t": "oscillation_state_topic",
"osc_val_tpl": "oscillation_value_template",
"pl": "payload",
"pl_arm_away": "payload_arm_away",
"pl_arm_home": "payload_arm_home",
"pl_arm_nite": "payload_arm_night",
@@ -142,6 +144,7 @@ ABBREVIATIONS = {
"stat_t": "state_topic",
"stat_tpl": "state_template",
"stat_val_tpl": "state_value_template",
"stype": "subtype",
"sup_feat": "supported_features",
"swing_mode_cmd_t": "swing_mode_command_topic",
"swing_mode_stat_tpl": "swing_mode_state_template",

View File

@@ -178,15 +178,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async def async_discover(discovery_payload):
"""Discover and add an MQTT cover."""
discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH)
try:
discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH)
config = PLATFORM_SCHEMA(discovery_payload)
await _async_setup_entity(
config, async_add_entities, config_entry, discovery_hash
)
except Exception:
if discovery_hash:
clear_discovery_hash(hass, discovery_hash)
clear_discovery_hash(hass, discovery_hash)
raise
async_dispatcher_connect(

View 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
)

View 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
)

View File

@@ -26,6 +26,7 @@ SUPPORTED_COMPONENTS = [
"camera",
"climate",
"cover",
"device_automation",
"fan",
"light",
"lock",
@@ -40,6 +41,7 @@ CONFIG_ENTRY_COMPONENTS = [
"camera",
"climate",
"cover",
"device_automation",
"fan",
"light",
"lock",
@@ -197,9 +199,15 @@ async def async_start(
config_entries_key = "{}.{}".format(component, "mqtt")
async with hass.data[DATA_CONFIG_ENTRY_LOCK]:
if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]:
await hass.config_entries.async_forward_entry_setup(
config_entry, component
)
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(
config_entry, component
)
hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key)
async_dispatcher_send(

View File

@@ -27,5 +27,27 @@
"error": {
"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"
}
}
}

View File

@@ -2,7 +2,7 @@
"domain": "nsw_rural_fire_service_feed",
"name": "NSW Rural Fire Service Incidents",
"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": [],
"codeowners": ["@exxamalte"]
}

View File

@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/octoprint",
"requirements": [],
"dependencies": [],
"after_dependencies": ["discovery"],
"codeowners": []
}

View File

@@ -4,7 +4,7 @@
"all_configured": "Tutti i server collegati sono gi\u00e0 configurati",
"already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato",
"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",
"non-interactive": "Importazione non interattiva",
"token_request_timeout": "Timeout per l'ottenimento del token",

View File

@@ -4,7 +4,7 @@
"all_configured": "Alle knyttet servere som allerede er konfigurert",
"already_configured": "Denne Plex-serveren er allerede 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",
"non-interactive": "Ikke-interaktiv import",
"token_request_timeout": "Tidsavbrudd ved innhenting av token",

View File

@@ -27,6 +27,7 @@ from homeassistant.helpers.dispatcher import (
)
from .const import (
CONF_IGNORE_NEW_SHARED_USERS,
CONF_SERVER,
CONF_SERVER_IDENTIFIER,
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_SHOW_ALL_CONTROLS, default=False): cv.boolean,
vol.Optional(CONF_IGNORE_NEW_SHARED_USERS, default=False): cv.boolean,
}
)

View File

@@ -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.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json
from .const import ( # pylint: disable=unused-import
AUTH_CALLBACK_NAME,
AUTH_CALLBACK_PATH,
CONF_CLIENT_IDENTIFIER,
CONF_IGNORE_NEW_SHARED_USERS,
CONF_MONITORED_USERS,
CONF_SERVER,
CONF_SERVER_IDENTIFIER,
CONF_SHOW_ALL_CONTROLS,
@@ -28,6 +31,7 @@ from .const import ( # pylint: disable=unused-import
DOMAIN,
PLEX_CONFIG_FILE,
PLEX_SERVER_CONFIG,
SERVERS,
X_PLEX_DEVICE_NAME,
X_PLEX_PLATFORM,
X_PLEX_PRODUCT,
@@ -254,6 +258,7 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry):
"""Initialize Plex options flow."""
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):
"""Manage the Plex options."""
@@ -261,6 +266,8 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_plex_mp_settings(self, user_input=None):
"""Manage the Plex media_player options."""
plex_server = self.hass.data[DOMAIN][SERVERS][self.server_id]
if user_input is not None:
self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] = user_input[
CONF_USE_EPISODE_ART
@@ -268,19 +275,56 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] = user_input[
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)
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(
step_id="plex_mp_settings",
data_schema=vol.Schema(
{
vol.Required(
CONF_USE_EPISODE_ART,
default=self.options[MP_DOMAIN][CONF_USE_EPISODE_ART],
default=plex_server.option_use_episode_art,
): bool,
vol.Required(
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,
}
),

View File

@@ -29,6 +29,8 @@ CONF_SERVER = "server"
CONF_SERVER_IDENTIFIER = "server_id"
CONF_USE_EPISODE_ART = "use_episode_art"
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_NAME = "auth:plex:callback"

View File

@@ -218,6 +218,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
if session_device:
self._make = session_device.device or ""
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_title = self._device_title or session_device.title
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_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:
# Add more context in name for likely duplicates
name_parts.append(self.make)
@@ -274,7 +275,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
thumb_url = self.session.thumbUrl
if (
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)
@@ -481,7 +482,7 @@ class PlexMediaPlayer(MediaPlayerDevice):
def supported_features(self):
"""Flag media player features that are supported."""
# force show all controls
if self.plex_server.show_all_controls:
if self.plex_server.option_show_all_controls:
return (
SUPPORT_PAUSE
| SUPPORT_PREVIOUS_TRACK
@@ -738,8 +739,8 @@ class PlexMediaPlayer(MediaPlayerDevice):
return {
"identifiers": {(PLEX_DOMAIN, self.machine_identifier)},
"manufacturer": "Plex",
"model": self._device_product or self._device_platform or self.make,
"manufacturer": self._device_platform or "Plex",
"model": self._device_product or self.make,
"name": self.name,
"sw_version": self._device_version,
"via_device": (PLEX_DOMAIN, self.plex_server.machine_identifier),

View File

@@ -13,6 +13,8 @@ from homeassistant.helpers.dispatcher import dispatcher_send
from .const import (
CONF_CLIENT_IDENTIFIER,
CONF_IGNORE_NEW_SHARED_USERS,
CONF_MONITORED_USERS,
CONF_SERVER,
CONF_SHOW_ALL_CONTROLS,
CONF_USE_EPISODE_ART,
@@ -51,6 +53,7 @@ class PlexServer:
self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
self.options = options
self.server_choice = None
self._accounts = []
self._owner_username = None
self._version = None
@@ -95,6 +98,12 @@ class PlexServer:
else:
_connect_with_token()
self._accounts = [
account.name
for account in self._plex_server.systemAccounts()
if account.name
]
owner_account = [
account.name
for account in self._plex_server.systemAccounts()
@@ -121,8 +130,22 @@ class PlexServer:
_LOGGER.debug("Updating devices")
available_clients = {}
ignored_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:
devices = self._plex_server.clients()
sessions = self._plex_server.sessions()
@@ -147,7 +170,12 @@ class PlexServer:
if session.TYPE == "photo":
_LOGGER.debug("Photo session detected, skipping: %s", session)
continue
session_username = session.usernames[0]
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)
available_clients.setdefault(
player.machineIdentifier, {"device": player}
@@ -160,6 +188,8 @@ class PlexServer:
new_entity_configs = []
for client_id, client_data in available_clients.items():
if client_id in ignored_clients:
continue
if client_id in new_clients:
new_entity_configs.append(client_data)
else:
@@ -167,11 +197,11 @@ class PlexServer:
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(
available_clients
)
idle_clients = (
self._known_clients - self._known_idle - ignored_clients
).difference(available_clients)
for client_id in idle_clients:
self.refresh_entity(client_id, None, None)
self._known_idle.add(client_id)
@@ -194,6 +224,11 @@ class PlexServer:
"""Return the plexapi PlexServer instance."""
return self._plex_server
@property
def accounts(self):
"""Return accounts associated with the Plex server."""
return set(self._accounts)
@property
def owner(self):
"""Return the Plex server owner username."""
@@ -220,15 +255,25 @@ class PlexServer:
return self._plex_server._baseurl # pylint: disable=protected-access
@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 self.options[MP_DOMAIN][CONF_USE_EPISODE_ART]
@property
def show_all_controls(self):
def option_show_all_controls(self):
"""Return show_all_controls option."""
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
def library(self):
"""Return library attribute from server object."""

View File

@@ -36,7 +36,9 @@
"description": "Options for Plex Media Players",
"data": {
"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"
}
}
}

View File

@@ -3,7 +3,7 @@
"name": "Sony PlayStation 4",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ps4",
"requirements": ["pyps4-2ndscreen==1.0.6"],
"requirements": ["pyps4-2ndscreen==1.0.7"],
"dependencies": [],
"codeowners": ["@ktnrg45"]
}

View File

@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/roku",
"requirements": ["roku==4.0.0"],
"dependencies": [],
"after_dependencies": ["discovery"],
"codeowners": []
}

View File

@@ -19,7 +19,7 @@ from homeassistant.components.light import (
SUPPORT_TRANSITION,
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
from homeassistant.helpers.restore_state import RestoreEntity
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.Optional(CONF_FREQUENCY): cv.positive_int,
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:
opt_args["freq"] = led_conf[CONF_FREQUENCY]
if driver_type == CONF_DRIVER_GPIO:
if CONF_HOST in led_conf:
opt_args["host"] = led_conf[CONF_HOST]
driver = GpioDriver(pins, **opt_args)
elif driver_type == CONF_DRIVER_PCA9685:
if CONF_ADDRESS in led_conf:

View File

@@ -2,7 +2,7 @@
"domain": "rpi_gpio_pwm",
"name": "pigpio Daemon PWM LED",
"documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm",
"requirements": ["pwmled==1.4.1"],
"requirements": ["pwmled==1.5.0"],
"dependencies": [],
"codeowners": []
}

View File

@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/sabnzbd",
"requirements": ["pysabnzbd==1.1.0"],
"dependencies": ["configurator"],
"after_dependencies": ["discovery"],
"codeowners": []
}

View File

@@ -15,7 +15,13 @@ from simplipy.websocket import (
import voluptuous as vol
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.exceptions import ConfigEntryNotReady
from homeassistant.helpers import (
@@ -59,6 +65,7 @@ DATA_LISTENER = "listener"
TOPIC_UPDATE = "simplisafe_update_data_{0}"
EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT"
EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION"
DEFAULT_SOCKET_MIN_RETRY = 15
DEFAULT_WATCHDOG_SECONDS = 5 * 60
@@ -71,6 +78,7 @@ WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT = [
EVENT_MOTION_DETECTED,
]
ATTR_CATEGORY = "category"
ATTR_LAST_EVENT_CHANGED_BY = "last_event_changed_by"
ATTR_LAST_EVENT_INFO = "last_event_info"
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_TYPE = "last_event_type"
ATTR_LAST_EVENT_TYPE = "last_event_type"
ATTR_MESSAGE = "message"
ATTR_PIN_LABEL = "label"
ATTR_PIN_LABEL_OR_VALUE = "label_or_pin"
ATTR_PIN_VALUE = "pin"
ATTR_SYSTEM_ID = "system_id"
ATTR_TIMESTAMP = "timestamp"
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._emergency_refresh_token_used = False
self._hass = hass
self._system_notifications = {}
self.initial_event_to_use = {}
self.systems = None
self.systems = {}
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):
"""Initialize the data class."""
asyncio.create_task(self.websocket.async_websocket_connect())
@@ -471,6 +514,7 @@ class SimpliSafe:
async def update_system(system):
"""Update a system."""
await system.update()
self._async_process_new_notifications(system)
_LOGGER.debug('Updated REST API data for "%s"', system.address)
async_dispatcher_send(self._hass, TOPIC_UPDATE.format(system.system_id))

View File

@@ -406,7 +406,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateDevice):
self._device.device_id,
mode,
)
self._hvac_modes = modes
self._hvac_modes = list(modes)
@property
def current_temperature(self):

View File

@@ -6,6 +6,7 @@ from twitch import TwitchClient
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_TOKEN
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -13,6 +14,13 @@ _LOGGER = logging.getLogger(__name__)
ATTR_GAME = "game"
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_CLIENT_ID = "client_id"
@@ -26,6 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_CLIENT_ID): 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."""
channels = config[CONF_CHANNELS]
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:
client.ingests.get_server_list()
except HTTPError:
_LOGGER.error("Client ID is not valid")
_LOGGER.error("Client ID or OAuth token is not valid")
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):
"""Representation of an Twitch channel."""
def __init__(self, user, client):
def __init__(self, channel, client):
"""Initialize the sensor."""
self._client = client
self._user = user
self._channel = self._user.name
self._id = self._user.id
self._state = self._preview = self._game = self._title = None
self._channel = channel
self._oauth_enabled = client._oauth_token is not None
self._state = None
self._preview = None
self._game = None
self._title = None
self._subscription = None
self._follow = None
self._statistics = None
@property
def should_poll(self):
@@ -66,7 +81,7 @@ class TwitchSensor(Entity):
@property
def name(self):
"""Return the name of the sensor."""
return self._channel
return self._channel.display_name
@property
def state(self):
@@ -81,28 +96,67 @@ class TwitchSensor(Entity):
@property
def device_state_attributes(self):
"""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:
return {ATTR_GAME: self._game, ATTR_TITLE: self._title}
attr.update({ATTR_GAME: self._game, ATTR_TITLE: self._title})
return attr
@property
def unique_id(self):
"""Return unique ID for this sensor."""
return self._id
return self._channel.id
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return ICON
# pylint: disable=no-member
def update(self):
"""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:
self._game = stream.get("channel").get("game")
self._title = stream.get("channel").get("status")
self._preview = stream.get("preview").get("medium")
self._game = stream.channel.get("game")
self._title = stream.channel.get("status")
self._preview = stream.preview.get("medium")
self._state = STATE_STREAMING
else:
self._preview = self._client.users.get_by_id(self._id).get("logo")
self._preview = self._channel.logo
self._state = STATE_OFFLINE

View File

@@ -28,15 +28,20 @@
"device_tracker": {
"data": {
"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_devices": "Track network devices (Ubiquiti devices)",
"track_wired_clients": "Include wired network clients"
}
},
"description": "Configure device tracking",
"title": "UniFi options"
},
"statistics_sensors": {
"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"
}
}
}

View File

@@ -33,12 +33,6 @@
"track_wired_clients": "Inkluder kablede nettverksklienter"
}
},
"init": {
"data": {
"one": "en",
"other": "andre"
}
},
"statistics_sensors": {
"data": {
"allow_bandwidth_sensors": "Opprett b\u00e5ndbreddesensorer for nettverksklienter"

View File

@@ -36,7 +36,7 @@
"init": {
"data": {
"one": "Tom",
"other": "Tom"
"other": "Tomma"
}
},
"statistics_sensors": {

View File

@@ -12,21 +12,18 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
)
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from .const import (
CONF_ALLOW_BANDWIDTH_SENSORS,
CONF_CONTROLLER,
CONF_DETECTION_TIME,
CONF_SITE_ID,
CONF_SSID_FILTER,
CONF_TRACK_CLIENTS,
CONF_TRACK_DEVICES,
CONF_TRACK_WIRED_CLIENTS,
CONTROLLER_ID,
DEFAULT_ALLOW_BANDWIDTH_SENSORS,
DEFAULT_DETECTION_TIME,
DEFAULT_TRACK_CLIENTS,
DEFAULT_TRACK_DEVICES,
DEFAULT_TRACK_WIRED_CLIENTS,
DOMAIN,
LOGGER,
)
@@ -185,33 +182,30 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
self.options.update(user_input)
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(
step_id="device_tracker",
data_schema=vol.Schema(
{
vol.Optional(
CONF_TRACK_CLIENTS,
default=self.config_entry.options.get(
CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS
),
CONF_TRACK_CLIENTS, default=controller.option_track_clients,
): bool,
vol.Optional(
CONF_TRACK_WIRED_CLIENTS,
default=self.config_entry.options.get(
CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS
),
default=controller.option_track_wired_clients,
): bool,
vol.Optional(
CONF_TRACK_DEVICES,
default=self.config_entry.options.get(
CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES
),
CONF_TRACK_DEVICES, default=controller.option_track_devices,
): bool,
vol.Optional(
CONF_SSID_FILTER, default=controller.option_ssid_filter
): cv.multi_select(ssid_filter),
vol.Optional(
CONF_DETECTION_TIME,
default=self.config_entry.options.get(
CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME
),
default=int(controller.option_detection_time.total_seconds()),
): int,
}
),
@@ -223,16 +217,15 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow):
self.options.update(user_input)
return await self._update_options()
controller = get_controller_from_config_entry(self.hass, self.config_entry)
return self.async_show_form(
step_id="statistics_sensors",
data_schema=vol.Schema(
{
vol.Optional(
CONF_ALLOW_BANDWIDTH_SENSORS,
default=self.config_entry.options.get(
CONF_ALLOW_BANDWIDTH_SENSORS,
DEFAULT_ALLOW_BANDWIDTH_SENSORS,
),
default=controller.option_allow_bandwidth_sensors,
): bool
}
),

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifi",
"requirements": [
"aiounifi==12"
"aiounifi==13"
],
"dependencies": [],
"codeowners": [

View File

@@ -31,15 +31,20 @@
"device_tracker": {
"data": {
"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_devices": "Track network devices (Ubiquiti devices)",
"track_wired_clients": "Include wired network clients"
}
},
"description": "Configure device tracking",
"title": "UniFi options"
},
"statistics_sensors": {
"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"
}
}
}

View File

@@ -2,7 +2,7 @@
"domain": "vallox",
"name": "Valloxs",
"documentation": "https://www.home-assistant.io/integrations/vallox",
"requirements": ["vallox-websocket-api==2.2.0"],
"requirements": ["vallox-websocket-api==2.4.0"],
"dependencies": [],
"codeowners": []
}

View File

@@ -1,11 +1,21 @@
{
"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": {
"user": {
"data": {
"access_token": "Toegangstoken voor de Vilfo Router API",
"host": "Router hostnaam of IP-adres"
}
},
"title": "Maak verbinding met de Vilfo Router"
}
}
},
"title": "Vilfo Router"
}
}

View 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"
}
}

View File

@@ -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.",
"host_exists": "Vizio komponent med vert allerede 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_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."
},

View File

@@ -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.",
"host_exists": "Vizio-komponenten med v\u00e4rdnamnet \u00e4r 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_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."
},

View File

@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara",
"requirements": ["PyXiaomiGateway==0.12.4"],
"dependencies": [],
"after_dependencies": ["discovery"],
"codeowners": ["@danielhiversen", "@syssi"]
}

View File

@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/yeelight",
"requirements": ["yeelight==0.5.0"],
"dependencies": [],
"after_dependencies": ["discovery"],
"codeowners": ["@rytilahti", "@zewelor"]
}

View File

@@ -108,7 +108,6 @@ def setup(hass, config):
def stop_zeroconf(_):
"""Stop Zeroconf."""
zeroconf.unregister_service(info)
zeroconf.close()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf)

View File

@@ -1288,6 +1288,9 @@ class Config:
# List of allowed external dirs to access
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]:
"""Calculate distance from Home Assistant.
@@ -1350,6 +1353,7 @@ class Config:
"whitelist_external_dirs": self.whitelist_external_dirs,
"version": __version__,
"config_source": self.config_source,
"safe_mode": self.safe_mode,
}
def set_time_zone(self, time_zone_str: str) -> None:

View File

@@ -392,11 +392,14 @@ class EntityRegistry:
unique_id=entity["unique_id"],
platform=entity["platform"],
name=entity.get("name"),
icon=entity.get("icon"),
disabled_by=entity.get("disabled_by"),
capabilities=entity.get("capabilities") or {},
supported_features=entity.get("supported_features", 0),
device_class=entity.get("device_class"),
unit_of_measurement=entity.get("unit_of_measurement"),
original_name=entity.get("original_name"),
original_icon=entity.get("original_icon"),
)
self.entities = entities
@@ -419,11 +422,14 @@ class EntityRegistry:
"unique_id": entry.unique_id,
"platform": entry.platform,
"name": entry.name,
"icon": entry.icon,
"disabled_by": entry.disabled_by,
"capabilities": entry.capabilities,
"supported_features": entry.supported_features,
"device_class": entry.device_class,
"unit_of_measurement": entry.unit_of_measurement,
"original_name": entry.original_name,
"original_icon": entry.original_icon,
}
for entry in self.entities.values()
]

View File

@@ -41,7 +41,6 @@ DATA_INTEGRATIONS = "integrations"
DATA_CUSTOM_COMPONENTS = "custom_components"
PACKAGE_CUSTOM_COMPONENTS = "custom_components"
PACKAGE_BUILTIN = "homeassistant.components"
LOOKUP_PATHS = [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]
CUSTOM_WARNING = (
"You are using a custom integration for %s which has not "
"been tested by Home Assistant. This component might "
@@ -67,6 +66,9 @@ async def _async_get_custom_components(
hass: "HomeAssistant",
) -> Dict[str, "Integration"]:
"""Return list of custom integrations."""
if hass.config.safe_mode:
return {}
try:
import custom_components
except ImportError:
@@ -178,7 +180,7 @@ class Integration:
Will create a stub manifest.
"""
comp = _load_file(hass, domain, LOOKUP_PATHS)
comp = _load_file(hass, domain, _lookup_path(hass))
if comp is None:
return None
@@ -464,7 +466,7 @@ class Components:
component: Optional[ModuleType] = integration.get_component()
else:
# 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:
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:
sys.path.insert(0, hass.config.config_dir)
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]

View File

@@ -11,7 +11,7 @@ cryptography==2.8
defusedxml==0.6.0
distro==1.4.0
hass-nabucasa==0.31
home-assistant-frontend==20200217.0
home-assistant-frontend==20200219.0
importlib-metadata==1.5.0
jinja2>=2.10.3
netdisco==2.6.0

View File

@@ -4,7 +4,7 @@ import json
import logging
import os
import tempfile
from typing import Dict, List, Optional, Type, Union
from typing import Any, Dict, List, Optional, Type, Union
from homeassistant.exceptions import HomeAssistantError
@@ -85,7 +85,7 @@ def save_json(
_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.
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:
json.dumps(obj)
valid = True
continue
except TypeError:
valid = False
pass
if isinstance(obj, dict):
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):
for idx, value in enumerate(obj):
to_process.append((value, f"{obj_path}[{idx}]"))
elif not valid: # type: ignore
else:
invalid.append(obj_path)
return invalid

View File

@@ -80,16 +80,19 @@ class AsyncHandler:
def _process(self) -> None:
"""Process log in a thread."""
while True:
record = asyncio.run_coroutine_threadsafe(
self._queue.get(), self.loop
).result()
try:
while True:
record = asyncio.run_coroutine_threadsafe(
self._queue.get(), self.loop
).result()
if record is None:
self.handler.close()
return
if record is None:
self.handler.close()
return
self.handler.emit(record)
self.handler.emit(record)
except asyncio.CancelledError:
self.handler.close()
def createLock(self) -> None:
"""Ignore lock stuff."""

View File

@@ -123,13 +123,13 @@ adguardhome==0.4.1
afsapi==0.0.4
# homeassistant.components.geonetnz_quakes
aio_geojson_geonetnz_quakes==0.11
aio_geojson_geonetnz_quakes==0.12
# homeassistant.components.geonetnz_volcano
aio_geojson_geonetnz_volcano==0.5
# homeassistant.components.nsw_rural_fire_service_feed
aio_geojson_nsw_rfs_incidents==0.1
aio_geojson_nsw_rfs_incidents==0.3
# homeassistant.components.gdacs
aio_georss_gdacs==0.3
@@ -199,7 +199,7 @@ aiopylgtv==0.3.3
aioswitcher==2019.4.26
# homeassistant.components.unifi
aiounifi==12
aiounifi==13
# homeassistant.components.wwlln
aiowwlln==2.0.2
@@ -305,7 +305,7 @@ beewi_smartclim==0.0.7
bellows-homeassistant==0.13.2
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.7.0
bimmer_connected==0.7.1
# homeassistant.components.bizkaibus
bizkaibus==0.1.1
@@ -683,7 +683,7 @@ hole==0.5.0
holidays==0.10.1
# homeassistant.components.frontend
home-assistant-frontend==20200217.0
home-assistant-frontend==20200219.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.8
@@ -1072,7 +1072,7 @@ pushetta==1.0.15
pushover_complete==1.1.1
# homeassistant.components.rpi_gpio_pwm
pwmled==1.4.1
pwmled==1.5.0
# homeassistant.components.august
py-august==0.14.0
@@ -1460,7 +1460,7 @@ pypjlink2==1.2.0
pypoint==1.1.2
# homeassistant.components.ps4
pyps4-2ndscreen==1.0.6
pyps4-2ndscreen==1.0.7
# homeassistant.components.qwikswitch
pyqwikswitch==0.93
@@ -2025,7 +2025,7 @@ uscisstatus==0.1.1
uvcclient==0.11.0
# homeassistant.components.vallox
vallox-websocket-api==2.2.0
vallox-websocket-api==2.4.0
# homeassistant.components.venstar
venstarcolortouch==0.12

View File

@@ -7,7 +7,7 @@ asynctest==0.13.0
codecov==2.0.15
mock-open==1.3.1
mypy==0.761
pre-commit==2.0.1
pre-commit==2.1.0
pylint==2.4.4
astroid==2.3.3
pylint-strict-informational==0.1

Some files were not shown because too many files have changed in this diff Show More