Merge pull request #20794 from home-assistant/rc

0.87.0
This commit is contained in:
Paulus Schoutsen 2019-02-06 14:57:53 -08:00 committed by GitHub
commit c366fa00d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
474 changed files with 15858 additions and 2795 deletions

View File

@ -19,6 +19,9 @@ omit =
homeassistant/components/alarmdecoder.py
homeassistant/components/*/alarmdecoder.py
homeassistant/components/ambient_station/__init__.py
homeassistant/components/ambient_station/sensor.py
homeassistant/components/amcrest.py
homeassistant/components/*/amcrest.py
@ -80,17 +83,24 @@ omit =
homeassistant/components/digital_ocean.py
homeassistant/components/*/digital_ocean.py
homeassistant/components/danfoss_air/*
homeassistant/components/dominos.py
homeassistant/components/doorbird.py
homeassistant/components/*/doorbird.py
homeassistant/components/dovado/*
homeassistant/components/dweet.py
homeassistant/components/*/dweet.py
homeassistant/components/eight_sleep.py
homeassistant/components/*/eight_sleep.py
homeassistant/components/ecoal_boiler.py
homeassistant/components/*/ecoal_boiler.py
homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py
@ -163,13 +173,13 @@ omit =
homeassistant/components/hlk_sw16.py
homeassistant/components/*/hlk_sw16.py
homeassistant/components/homekit_controller/__init__.py
homeassistant/components/*/homekit_controller.py
homeassistant/components/homekit_controller/*
homeassistant/components/homematic/__init__.py
homeassistant/components/*/homematic.py
homeassistant/components/homematicip_cloud.py
homeassistant/components/homematicip_cloud/hap.py
homeassistant/components/homematicip_cloud/device.py
homeassistant/components/*/homematicip_cloud.py
homeassistant/components/homeworks.py
@ -385,6 +395,9 @@ omit =
homeassistant/components/tradfri.py
homeassistant/components/*/tradfri.py
homeassistant/components/transmission.py
homeassistant/components/*/transmission.py
homeassistant/components/notify/twilio_sms.py
homeassistant/components/notify/twilio_call.py
@ -443,15 +456,19 @@ omit =
homeassistant/components/zha/sensor.py
homeassistant/components/zha/switch.py
homeassistant/components/zha/api.py
homeassistant/components/zha/entities/*
homeassistant/components/zha/helpers.py
homeassistant/components/zha/entity.py
homeassistant/components/zha/device_entity.py
homeassistant/components/zha/core/helpers.py
homeassistant/components/zha/core/const.py
homeassistant/components/zha/core/device.py
homeassistant/components/zha/core/listeners.py
homeassistant/components/zha/core/gateway.py
homeassistant/components/*/zha.py
homeassistant/components/zigbee.py
homeassistant/components/*/zigbee.py
homeassistant/components/zoneminder/*
homeassistant/components/*/zoneminder.py
homeassistant/components/tuya.py
homeassistant/components/*/tuya.py
@ -460,6 +477,7 @@ omit =
homeassistant/components/*/spider.py
homeassistant/components/air_quality/opensensemap.py
homeassistant/components/air_quality/nilu.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py
@ -552,6 +570,7 @@ omit =
homeassistant/components/device_tracker/sky_hub.py
homeassistant/components/device_tracker/snmp.py
homeassistant/components/device_tracker/swisscom.py
homeassistant/components/device_tracker/synology_srm.py
homeassistant/components/device_tracker/tado.py
homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tile.py
@ -574,6 +593,7 @@ omit =
homeassistant/components/image_processing/dlib_face_identify.py
homeassistant/components/image_processing/seven_segments.py
homeassistant/components/image_processing/tensorflow.py
homeassistant/components/image_processing/qrcode.py
homeassistant/components/keyboard_remote.py
homeassistant/components/keyboard.py
homeassistant/components/light/avion.py
@ -581,6 +601,7 @@ omit =
homeassistant/components/light/blinkt.py
homeassistant/components/light/decora_wifi.py
homeassistant/components/light/decora.py
homeassistant/components/light/everlights.py
homeassistant/components/light/flux_led.py
homeassistant/components/light/futurenow.py
homeassistant/components/light/greenwave.py
@ -719,7 +740,6 @@ omit =
homeassistant/components/sensor/aftership.py
homeassistant/components/sensor/airvisual.py
homeassistant/components/sensor/alpha_vantage.py
homeassistant/components/sensor/ambient_station.py
homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py
homeassistant/components/sensor/bbox.py
@ -744,7 +764,6 @@ omit =
homeassistant/components/sensor/dht.py
homeassistant/components/sensor/discogs.py
homeassistant/components/sensor/dnsip.py
homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/domain_expiry.py
homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/dublin_bus_transport.py
@ -781,6 +800,7 @@ omit =
homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py
homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/iliad_italy.py
homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/influxdb.py
@ -835,7 +855,9 @@ omit =
homeassistant/components/sensor/qnap.py
homeassistant/components/sensor/radarr.py
homeassistant/components/sensor/rainbird.py
homeassistant/components/sensor/recollect_waste.py
homeassistant/components/sensor/ripple.py
homeassistant/components/sensor/rova.py
homeassistant/components/sensor/rtorrent.py
homeassistant/components/sensor/ruter.py
homeassistant/components/sensor/scrape.py
@ -874,7 +896,6 @@ omit =
homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/torque.py
homeassistant/components/sensor/trafikverket_weatherstation.py
homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/travisci.py
homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py
@ -919,7 +940,6 @@ omit =
homeassistant/components/switch/switchmate.py
homeassistant/components/switch/telnet.py
homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/vesync.py
homeassistant/components/telegram_bot/*
homeassistant/components/thingspeak.py

View File

@ -95,6 +95,7 @@ homeassistant/components/notify/syslog.py @fabaff
homeassistant/components/notify/xmpp.py @fabaff
homeassistant/components/notify/yessssms.py @flowolf
homeassistant/components/plant.py @ChristianKuehnel
homeassistant/components/remote/harmony.py @ehendrix23
homeassistant/components/scene/lifx_cloud.py @amelchio
homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/alpha_vantage.py @fabaff
@ -152,6 +153,7 @@ homeassistant/components/weather/openweathermap.py @fabaff
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
# A
homeassistant/components/ambient_station/* @bachya
homeassistant/components/arduino.py @fabaff
homeassistant/components/*/arduino.py @fabaff
homeassistant/components/*/arest.py @fabaff
@ -234,6 +236,7 @@ homeassistant/components/*/rfxtrx.py @danielhiversen
# S
homeassistant/components/simplisafe/* @bachya
homeassistant/components/smartthings/* @andrewsayre
# T
homeassistant/components/tahoma.py @philklei
@ -268,8 +271,7 @@ homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
# Z
homeassistant/components/zoneminder/ @rohankapoorcom
homeassistant/components/*/zoneminder.py @rohankapoorcom
homeassistant/components/zoneminder/* @rohankapoorcom
# Other code
homeassistant/scripts/check_config.py @kellerza

View File

@ -2,7 +2,8 @@
import base64
from collections import OrderedDict
import logging
from typing import Any, Dict, List, Optional, cast
from typing import Any, Dict, List, Optional, Set, cast # noqa: F401
import bcrypt
import voluptuous as vol
@ -52,6 +53,9 @@ class Data:
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
private=True)
self._data = None # type: Optional[Dict[str, Any]]
# Legacy mode will allow usernames to start/end with whitespace
# and will compare usernames case-insensitive.
# Remove in 2020 or when we launch 1.0.
self.is_legacy = False
@callback
@ -60,7 +64,7 @@ class Data:
if self.is_legacy:
return username
return username.strip()
return username.strip().casefold()
async def async_load(self) -> None:
"""Load stored data."""
@ -71,9 +75,26 @@ class Data:
'users': []
}
seen = set() # type: Set[str]
for user in data['users']:
username = user['username']
# check if we have duplicates
folded = username.casefold()
if folded in seen:
self.is_legacy = True
logging.getLogger(__name__).warning(
"Home Assistant auth provider is running in legacy mode "
"because we detected usernames that are case-insensitive"
"equivalent. Please change the username: '%s'.", username)
break
seen.add(folded)
# check if we have unstripped usernames
if username != username.strip():
self.is_legacy = True
@ -81,7 +102,7 @@ class Data:
logging.getLogger(__name__).warning(
"Home Assistant auth provider is running in legacy mode "
"because we detected usernames that start or end in a "
"space. Please change the username.")
"space. Please change the username: '%s'.", username)
break
@ -103,7 +124,7 @@ class Data:
# Compare all users to avoid timing attacks.
for user in self.users:
if username == user['username']:
if self.normalize_username(user['username']) == username:
found = user
if found is None:

View File

@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
ATTR_AQI = 'air_quality_index'
ATTR_ATTRIBUTION = 'attribution'
ATTR_C02 = 'carbon_dioxide'
ATTR_CO2 = 'carbon_dioxide'
ATTR_CO = 'carbon_monoxide'
ATTR_N2O = 'nitrogen_oxide'
ATTR_NO = 'nitrogen_monoxide'
@ -35,7 +35,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
PROP_TO_ATTR = {
'air_quality_index': ATTR_AQI,
'attribution': ATTR_ATTRIBUTION,
'carbon_dioxide': ATTR_C02,
'carbon_dioxide': ATTR_CO2,
'carbon_monoxide': ATTR_CO,
'nitrogen_oxide': ATTR_N2O,
'nitrogen_monoxide': ATTR_NO,

View File

@ -0,0 +1,252 @@
"""
Sensor for checking the air quality around Norway.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/air_quality.nilu/
"""
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.components.air_quality import (
PLATFORM_SCHEMA, AirQualityEntity)
from homeassistant.const import (
CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_SHOW_ON_MAP)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
REQUIREMENTS = ['niluclient==0.1.2']
_LOGGER = logging.getLogger(__name__)
ATTR_AREA = 'area'
ATTR_POLLUTION_INDEX = 'nilu_pollution_index'
ATTRIBUTION = "Data provided by luftkvalitet.info and nilu.no"
CONF_AREA = 'area'
CONF_STATION = 'stations'
DEFAULT_NAME = 'NILU'
SCAN_INTERVAL = timedelta(minutes=30)
CONF_ALLOWED_AREAS = [
'Bergen',
'Birkenes',
'Bodø',
'Brumunddal',
'Bærum',
'Drammen',
'Elverum',
'Fredrikstad',
'Gjøvik',
'Grenland',
'Halden',
'Hamar',
'Harstad',
'Hurdal',
'Karasjok',
'Kristiansand',
'Kårvatn',
'Lillehammer',
'Lillesand',
'Lillestrøm',
'Lørenskog',
'Mo i Rana',
'Moss',
'Narvik',
'Oslo',
'Prestebakke',
'Sandve',
'Sarpsborg',
'Stavanger',
'Sør-Varanger',
'Tromsø',
'Trondheim',
'Tustervatn',
'Zeppelinfjellet',
'Ålesund',
]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Inclusive(CONF_LATITUDE, 'coordinates',
'Latitude and longitude must exist together'): cv.latitude,
vol.Inclusive(CONF_LONGITUDE, 'coordinates',
'Latitude and longitude must exist together'): cv.longitude,
vol.Exclusive(CONF_AREA, 'station_collection',
'Can only configure one specific station or '
'stations in a specific area pr sensor. '
'Please only configure station or area.'
): vol.All(cv.string, vol.In(CONF_ALLOWED_AREAS)),
vol.Exclusive(CONF_STATION, 'station_collection',
'Can only configure one specific station or '
'stations in a specific area pr sensor. '
'Please only configure station or area.'
): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean,
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the NILU air quality sensor."""
import niluclient as nilu
name = config.get(CONF_NAME)
area = config.get(CONF_AREA)
stations = config.get(CONF_STATION)
show_on_map = config.get(CONF_SHOW_ON_MAP)
sensors = []
if area:
stations = nilu.lookup_stations_in_area(area)
elif not area and not stations:
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
location_client = nilu.create_location_client(latitude, longitude)
stations = location_client.station_names
for station in stations:
client = NiluData(nilu.create_station_client(station))
client.update()
if client.data.sensors:
sensors.append(NiluSensor(client, name, show_on_map))
else:
_LOGGER.warning("%s didn't give any sensors results", station)
add_entities(sensors, True)
class NiluData:
"""Class for handling the data retrieval."""
def __init__(self, api):
"""Initialize the data object."""
self.api = api
@property
def data(self):
"""Get data cached in client."""
return self.api.data
@Throttle(SCAN_INTERVAL)
def update(self):
"""Get the latest data from nilu API."""
self.api.update()
class NiluSensor(AirQualityEntity):
"""Single nilu station air sensor."""
def __init__(self, api_data: NiluData, name: str, show_on_map: bool):
"""Initialize the sensor."""
self._api = api_data
self._name = "{} {}".format(name, api_data.data.name)
self._max_aqi = None
self._attrs = {}
if show_on_map:
self._attrs[CONF_LATITUDE] = api_data.data.latitude
self._attrs[CONF_LONGITUDE] = api_data.data.longitude
@property
def attribution(self) -> str:
"""Return the attribution."""
return ATTRIBUTION
@property
def device_state_attributes(self) -> dict:
"""Return other details about the sensor state."""
return self._attrs
@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
def air_quality_index(self) -> str:
"""Return the Air Quality Index (AQI)."""
return self._max_aqi
@property
def carbon_monoxide(self) -> str:
"""Return the CO (carbon monoxide) level."""
from niluclient import CO
return self.get_component_state(CO)
@property
def carbon_dioxide(self) -> str:
"""Return the CO2 (carbon dioxide) level."""
from niluclient import CO2
return self.get_component_state(CO2)
@property
def nitrogen_oxide(self) -> str:
"""Return the N2O (nitrogen oxide) level."""
from niluclient import NOX
return self.get_component_state(NOX)
@property
def nitrogen_monoxide(self) -> str:
"""Return the NO (nitrogen monoxide) level."""
from niluclient import NO
return self.get_component_state(NO)
@property
def nitrogen_dioxide(self) -> str:
"""Return the NO2 (nitrogen dioxide) level."""
from niluclient import NO2
return self.get_component_state(NO2)
@property
def ozone(self) -> str:
"""Return the O3 (ozone) level."""
from niluclient import OZONE
return self.get_component_state(OZONE)
@property
def particulate_matter_2_5(self) -> str:
"""Return the particulate matter 2.5 level."""
from niluclient import PM25
return self.get_component_state(PM25)
@property
def particulate_matter_10(self) -> str:
"""Return the particulate matter 10 level."""
from niluclient import PM10
return self.get_component_state(PM10)
@property
def particulate_matter_0_1(self) -> str:
"""Return the particulate matter 0.1 level."""
from niluclient import PM1
return self.get_component_state(PM1)
@property
def sulphur_dioxide(self) -> str:
"""Return the SO2 (sulphur dioxide) level."""
from niluclient import SO2
return self.get_component_state(SO2)
def get_component_state(self, component_name: str) -> str:
"""Return formatted value of specified component."""
if component_name in self._api.data.sensors:
sensor = self._api.data.sensors[component_name]
return sensor.value
return None
def update(self) -> None:
"""Update the sensor."""
import niluclient as nilu
self._api.update()
sensors = self._api.data.sensors.values()
if sensors:
max_index = max([s.pollution_index for s in sensors])
self._max_aqi = max_index
self._attrs[ATTR_POLLUTION_INDEX] = \
nilu.POLLUTION_INDEX[self._max_aqi]
self._attrs[ATTR_AREA] = self._api.data.area

View File

@ -13,7 +13,8 @@ from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.helpers.config_validation import ( # noqa
PLATFORM_SCHEMA_BASE, PLATFORM_SCHEMA_2 as PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent

View File

@ -6,9 +6,9 @@ https://home-assistant.io/components/alarm_control_panel.abode/
"""
import logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice
from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED)
@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(alarm_devices)
class AbodeAlarm(AbodeDevice, AlarmControlPanel):
class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel):
"""An alarm_control_panel implementation for Abode."""
def __init__(self, data, device, name):
@ -57,6 +57,11 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanel):
state = None
return state
@property
def code_format(self):
"""Return one or more digits/characters."""
return alarm.FORMAT_NUMBER
def alarm_disarm(self, code=None):
"""Send disarm command."""
self._device.set_standby()

View File

@ -13,7 +13,7 @@ import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
@ -57,7 +57,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
self._username = username
self._password = password
self._websession = async_get_clientsession(self._hass)
self._state = STATE_UNKNOWN
self._state = None
self._alarm = Alarmdotcom(
username, password, self._websession, hass.loop)
@ -93,7 +93,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
return STATE_ALARM_ARMED_HOME
if self._alarm.state.lower() == 'armed away':
return STATE_ALARM_ARMED_AWAY
return STATE_UNKNOWN
return None
@property
def device_state_attributes(self):

View File

@ -5,18 +5,17 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.concord232/
"""
import datetime
from datetime import timedelta
import logging
import requests
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
import homeassistant.helpers.config_validation as cv
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
REQUIREMENTS = ['concord232==0.15']
@ -26,7 +25,7 @@ DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'CONCORD232'
DEFAULT_PORT = 5007
SCAN_INTERVAL = timedelta(seconds=10)
SCAN_INTERVAL = datetime.timedelta(seconds=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
@ -44,33 +43,24 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
url = 'http://{}:{}'.format(host, port)
try:
add_entities([Concord232Alarm(hass, url, name)])
add_entities([Concord232Alarm(url, name)], True)
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
return
class Concord232Alarm(alarm.AlarmControlPanel):
"""Representation of the Concord232-based alarm panel."""
def __init__(self, hass, url, name):
def __init__(self, url, name):
"""Initialize the Concord232 alarm panel."""
from concord232 import client as concord232_client
self._state = STATE_UNKNOWN
self._hass = hass
self._state = None
self._name = name
self._url = url
try:
client = concord232_client.Client(self._url)
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
self._alarm = client
self._alarm = concord232_client.Client(self._url)
self._alarm.partitions = self._alarm.list_partitions()
self._alarm.last_partition_update = datetime.datetime.now()
self.update()
@property
def name(self):
@ -94,22 +84,17 @@ class Concord232Alarm(alarm.AlarmControlPanel):
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
dict(host=self._url, reason=ex))
newstate = STATE_UNKNOWN
return
except IndexError:
_LOGGER.error("Concord232 reports no partitions")
newstate = STATE_UNKNOWN
return
if part['arming_level'] == 'Off':
newstate = STATE_ALARM_DISARMED
self._state = STATE_ALARM_DISARMED
elif 'Home' in part['arming_level']:
newstate = STATE_ALARM_ARMED_HOME
self._state = STATE_ALARM_ARMED_HOME
else:
newstate = STATE_ALARM_ARMED_AWAY
if not newstate == self._state:
_LOGGER.info("State change from %s to %s", self._state, newstate)
self._state = newstate
return self._state
self._state = STATE_ALARM_ARMED_AWAY
def alarm_disarm(self, code=None):
"""Send disarm command."""

View File

@ -76,7 +76,7 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
await self._home.set_security_zones_activation(True, False)
await self._home.set_security_zones_activation(False, True)
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""

View File

@ -14,7 +14,7 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME,
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, CONF_NAME,
STATE_ALARM_ARMED_CUSTOM_BYPASS)
@ -52,7 +52,7 @@ class TotalConnect(alarm.AlarmControlPanel):
self._name = name
self._username = username
self._password = password
self._state = STATE_UNKNOWN
self._state = None
self._client = TotalConnectClient.TotalConnectClient(
username, password)
@ -85,7 +85,7 @@ class TotalConnect(alarm.AlarmControlPanel):
elif status == self._client.DISARMING:
state = STATE_ALARM_DISARMING
else:
state = STATE_UNKNOWN
state = None
self._state = state

View File

@ -11,8 +11,7 @@ import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.verisure import CONF_ALARM, CONF_CODE_DIGITS
from homeassistant.components.verisure import HUB as hub
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN)
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
_LOGGER = logging.getLogger(__name__)
@ -44,7 +43,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
def __init__(self):
"""Initialize the Verisure alarm panel."""
self._state = STATE_UNKNOWN
self._state = None
self._digits = hub.config.get(CONF_CODE_DIGITS)
self._changed_by = None

View File

@ -9,8 +9,7 @@ import logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.wink import DOMAIN, WinkDevice
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN)
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
_LOGGER = logging.getLogger(__name__)
@ -52,7 +51,7 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
elif wink_state == "night":
state = STATE_ALARM_ARMED_HOME
else:
state = STATE_UNKNOWN
state = None
return state
def alarm_disarm(self, code=None):

View File

@ -5,19 +5,19 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alert/
"""
import asyncio
from datetime import datetime, timedelta
import logging
from datetime import datetime, timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_MESSAGE, DOMAIN as DOMAIN_NOTIFY)
ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA, DOMAIN as DOMAIN_NOTIFY)
from homeassistant.const import (
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers import service, event
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import ToggleEntity
_LOGGER = logging.getLogger(__name__)
@ -30,6 +30,8 @@ CONF_REPEAT = 'repeat'
CONF_SKIP_FIRST = 'skip_first'
CONF_ALERT_MESSAGE = 'message'
CONF_DONE_MESSAGE = 'done_message'
CONF_TITLE = 'title'
CONF_DATA = 'data'
DEFAULT_CAN_ACK = True
DEFAULT_SKIP_FIRST = False
@ -43,13 +45,14 @@ ALERT_SCHEMA = vol.Schema({
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
vol.Optional(CONF_ALERT_MESSAGE): cv.template,
vol.Optional(CONF_DONE_MESSAGE): cv.template,
vol.Optional(CONF_TITLE): cv.template,
vol.Optional(CONF_DATA): dict,
vol.Required(CONF_NOTIFIERS): cv.ensure_list})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA),
}, extra=vol.ALLOW_EXTRA)
ALERT_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
})
@ -77,12 +80,14 @@ async def async_setup(hass, config):
done_message_template = cfg.get(CONF_DONE_MESSAGE)
notifiers = cfg.get(CONF_NOTIFIERS)
can_ack = cfg.get(CONF_CAN_ACK)
title_template = cfg.get(CONF_TITLE)
data = cfg.get(CONF_DATA)
entities.append(Alert(hass, object_id, name,
watched_entity_id, alert_state, repeat,
skip_first, message_template,
done_message_template, notifiers,
can_ack))
can_ack, title_template, data))
if not entities:
return False
@ -127,12 +132,14 @@ class Alert(ToggleEntity):
def __init__(self, hass, entity_id, name, watched_entity_id,
state, repeat, skip_first, message_template,
done_message_template, notifiers, can_ack):
done_message_template, notifiers, can_ack, title_template,
data):
"""Initialize the alert."""
self.hass = hass
self._name = name
self._alert_state = state
self._skip_first = skip_first
self._data = data
self._message_template = message_template
if self._message_template is not None:
@ -142,6 +149,10 @@ class Alert(ToggleEntity):
if self._done_message_template is not None:
self._done_message_template.hass = hass
self._title_template = title_template
if self._title_template is not None:
self._title_template.hass = hass
self._notifiers = notifiers
self._can_ack = can_ack
@ -251,9 +262,20 @@ class Alert(ToggleEntity):
await self._send_notification_message(message)
async def _send_notification_message(self, message):
msg_payload = {ATTR_MESSAGE: message}
if self._title_template is not None:
title = self._title_template.async_render()
msg_payload.update({ATTR_TITLE: title})
if self._data:
msg_payload.update({ATTR_DATA: self._data})
_LOGGER.debug(msg_payload)
for target in self._notifiers:
await self.hass.services.async_call(
DOMAIN_NOTIFY, target, {ATTR_MESSAGE: message})
DOMAIN_NOTIFY, target, msg_payload)
async def async_turn_on(self, **kwargs):
"""Async Unacknowledge alert."""

View File

@ -0,0 +1,19 @@
{
"config": {
"error": {
"identifier_exists": "Clau d'aplicaci\u00f3 i/o clau API ja registrada",
"invalid_key": "Clau API i/o clau d'aplicaci\u00f3 inv\u00e0lida/es",
"no_devices": "No s'ha trobat cap dispositiu al compte"
},
"step": {
"user": {
"data": {
"api_key": "Clau API",
"app_key": "Clau d'aplicaci\u00f3"
},
"title": "Introdueix la teva informaci\u00f3"
}
},
"title": "Ambient PWS"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"error": {
"identifier_exists": "Application Key and/or API Key already registered",
"invalid_key": "Invalid API Key and/or Application Key",
"no_devices": "No devices found in account"
},
"step": {
"user": {
"data": {
"api_key": "API Key",
"app_key": "Application Key"
},
"title": "Fill in your information"
}
},
"title": "Ambient PWS"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"error": {
"identifier_exists": "Application \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"invalid_key": "Application \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"no_devices": "\uacc4\uc815\uc5d0 \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4"
},
"step": {
"user": {
"data": {
"api_key": "API \ud0a4",
"app_key": "Application \ud0a4"
},
"title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694"
}
},
"title": "Ambient PWS"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"error": {
"identifier_exists": "Applikatioun's Schl\u00ebssel an/oder API Schl\u00ebssel ass scho registr\u00e9iert",
"invalid_key": "Ong\u00ebltegen API Schl\u00ebssel an/oder Applikatioun's Schl\u00ebssel",
"no_devices": "Keng Apparater am Kont fonnt"
},
"step": {
"user": {
"data": {
"api_key": "API Schl\u00ebssel",
"app_key": "Applikatioun's Schl\u00ebssel"
},
"title": "F\u00ebllt \u00e4r Informatiounen aus"
}
},
"title": "Ambient PWS"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"error": {
"identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d",
"invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f",
"no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b"
},
"step": {
"user": {
"data": {
"api_key": "\u041a\u043b\u044e\u0447 API",
"app_key": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f"
},
"title": "Ambient PWS"
}
},
"title": "Ambient PWS"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"error": {
"identifier_exists": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u5df2\u8a3b\u518a",
"invalid_key": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u7121\u6548",
"no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e"
},
"step": {
"user": {
"data": {
"api_key": "API \u5bc6\u9470",
"app_key": "\u61c9\u7528\u5bc6\u9470"
},
"title": "\u586b\u5beb\u8cc7\u8a0a"
}
},
"title": "\u74b0\u5883 PWS"
}
}

View File

@ -0,0 +1,205 @@
"""
Support for Ambient Weather Station Service.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ambient_station/
"""
import logging
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_NAME, ATTR_LOCATION, CONF_API_KEY, CONF_MONITORED_CONDITIONS,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later
from .config_flow import configured_instances
from .const import (
ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE)
REQUIREMENTS = ['aioambient==0.1.0']
_LOGGER = logging.getLogger(__name__)
DEFAULT_SOCKET_MIN_RETRY = 15
SENSOR_TYPES = {
'24hourrainin': ('24 Hr Rain', 'in'),
'baromabsin': ('Abs Pressure', 'inHg'),
'baromrelin': ('Rel Pressure', 'inHg'),
'battout': ('Battery', ''),
'co2': ('co2', 'ppm'),
'dailyrainin': ('Daily Rain', 'in'),
'dewPoint': ('Dew Point', '°F'),
'eventrainin': ('Event Rain', 'in'),
'feelsLike': ('Feels Like', '°F'),
'hourlyrainin': ('Hourly Rain Rate', 'in/hr'),
'humidity': ('Humidity', '%'),
'humidityin': ('Humidity In', '%'),
'lastRain': ('Last Rain', ''),
'maxdailygust': ('Max Gust', 'mph'),
'monthlyrainin': ('Monthly Rain', 'in'),
'solarradiation': ('Solar Rad', 'W/m^2'),
'tempf': ('Temp', '°F'),
'tempinf': ('Inside Temp', '°F'),
'totalrainin': ('Lifetime Rain', 'in'),
'uv': ('uv', 'Index'),
'weeklyrainin': ('Weekly Rain', 'in'),
'winddir': ('Wind Dir', '°'),
'winddir_avg10m': ('Wind Dir Avg 10m', '°'),
'winddir_avg2m': ('Wind Dir Avg 2m', 'mph'),
'windgustdir': ('Gust Dir', '°'),
'windgustmph': ('Wind Gust', 'mph'),
'windspdmph_avg10m': ('Wind Avg 10m', 'mph'),
'windspdmph_avg2m': ('Wind Avg 2m', 'mph'),
'windspeedmph': ('Wind Speed', 'mph'),
'yearlyrainin': ('Yearly Rain', 'in'),
}
CONFIG_SCHEMA = vol.Schema({
DOMAIN:
vol.Schema({
vol.Required(CONF_APP_KEY):
cv.string,
vol.Required(CONF_API_KEY):
cv.string,
vol.Optional(
CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
"""Set up the Ambient PWS component."""
hass.data[DOMAIN] = {}
hass.data[DOMAIN][DATA_CLIENT] = {}
if DOMAIN not in config:
return True
conf = config[DOMAIN]
if conf[CONF_APP_KEY] in configured_instances(hass):
return True
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={'source': SOURCE_IMPORT}, data=conf))
return True
async def async_setup_entry(hass, config_entry):
"""Set up the Ambient PWS as config entry."""
from aioambient import Client
from aioambient.errors import WebsocketConnectionError
session = aiohttp_client.async_get_clientsession(hass)
try:
ambient = AmbientStation(
hass,
config_entry,
Client(
config_entry.data[CONF_API_KEY],
config_entry.data[CONF_APP_KEY], session),
config_entry.data.get(
CONF_MONITORED_CONDITIONS, list(SENSOR_TYPES)))
hass.loop.create_task(ambient.ws_connect())
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient
except WebsocketConnectionError as err:
_LOGGER.error('Config entry failed: %s', err)
raise ConfigEntryNotReady
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, ambient.client.websocket.disconnect())
return True
async def async_unload_entry(hass, config_entry):
"""Unload an Ambient PWS config entry."""
ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id)
hass.async_create_task(ambient.ws_disconnect())
await hass.config_entries.async_forward_entry_unload(
config_entry, 'sensor')
return True
class AmbientStation:
"""Define a class to handle the Ambient websocket."""
def __init__(self, hass, config_entry, client, monitored_conditions):
"""Initialize."""
self._config_entry = config_entry
self._hass = hass
self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
self.client = client
self.monitored_conditions = monitored_conditions
self.stations = {}
async def ws_connect(self):
"""Register handlers and connect to the websocket."""
from aioambient.errors import WebsocketError
def on_connect():
"""Define a handler to fire when the websocket is connected."""
_LOGGER.info('Connected to websocket')
def on_data(data):
"""Define a handler to fire when the data is received."""
mac_address = data['macAddress']
if data != self.stations[mac_address][ATTR_LAST_DATA]:
_LOGGER.debug('New data received: %s', data)
self.stations[mac_address][ATTR_LAST_DATA] = data
async_dispatcher_send(self._hass, TOPIC_UPDATE)
def on_disconnect():
"""Define a handler to fire when the websocket is disconnected."""
_LOGGER.info('Disconnected from websocket')
def on_subscribed(data):
"""Define a handler to fire when the subscription is set."""
for station in data['devices']:
if station['macAddress'] in self.stations:
continue
_LOGGER.debug('New station subscription: %s', data)
self.stations[station['macAddress']] = {
ATTR_LAST_DATA: station['lastData'],
ATTR_LOCATION: station['info']['location'],
ATTR_NAME: station['info']['name'],
}
self._hass.async_create_task(
self._hass.config_entries.async_forward_entry_setup(
self._config_entry, 'sensor'))
self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
self.client.websocket.on_connect(on_connect)
self.client.websocket.on_data(on_data)
self.client.websocket.on_disconnect(on_disconnect)
self.client.websocket.on_subscribed(on_subscribed)
try:
await self.client.websocket.connect()
except WebsocketError as err:
_LOGGER.error("Error with the websocket connection: %s", err)
self._ws_reconnect_delay = min(
2 * self._ws_reconnect_delay, 480)
async_call_later(
self._hass, self._ws_reconnect_delay, self.ws_connect)
async def ws_disconnect(self):
"""Disconnect from the websocket."""
await self.client.websocket.disconnect()

View File

@ -0,0 +1,72 @@
"""Config flow to configure the Ambient PWS component."""
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from .const import CONF_APP_KEY, DOMAIN
@callback
def configured_instances(hass):
"""Return a set of configured Ambient PWS instances."""
return set(
entry.data[CONF_APP_KEY]
for entry in hass.config_entries.async_entries(DOMAIN))
@config_entries.HANDLERS.register(DOMAIN)
class AmbientStationFlowHandler(config_entries.ConfigFlow):
"""Handle an Ambient PWS config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH
async def _show_form(self, errors=None):
"""Show the form to the user."""
data_schema = vol.Schema({
vol.Required(CONF_API_KEY): str,
vol.Required(CONF_APP_KEY): str,
})
return self.async_show_form(
step_id='user',
data_schema=data_schema,
errors=errors if errors else {},
)
async def async_step_import(self, import_config):
"""Import a config entry from configuration.yaml."""
return await self.async_step_user(import_config)
async def async_step_user(self, user_input=None):
"""Handle the start of the config flow."""
from aioambient import Client
from aioambient.errors import AmbientError
if not user_input:
return await self._show_form()
if user_input[CONF_APP_KEY] in configured_instances(self.hass):
return await self._show_form({CONF_APP_KEY: 'identifier_exists'})
session = aiohttp_client.async_get_clientsession(self.hass)
client = Client(
user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session)
try:
devices = await client.api.get_devices()
except AmbientError:
return await self._show_form({'base': 'invalid_key'})
if not devices:
return await self._show_form({'base': 'no_devices'})
# The Application Key (which identifies each config entry) is too long
# to show nicely in the UI, so we take the first 12 characters (similar
# to how GitHub does it):
return self.async_create_entry(
title=user_input[CONF_APP_KEY][:12], data=user_input)

View File

@ -0,0 +1,10 @@
"""Define constants for the Ambient PWS component."""
DOMAIN = 'ambient_station'
ATTR_LAST_DATA = 'last_data'
CONF_APP_KEY = 'app_key'
DATA_CLIENT = 'data_client'
TOPIC_UPDATE = 'update'

View File

@ -0,0 +1,102 @@
"""
Support for Ambient Weather Station Service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.ambient_station/
"""
import logging
from homeassistant.components.ambient_station import SENSOR_TYPES
from homeassistant.helpers.entity import Entity
from homeassistant.const import ATTR_NAME
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TOPIC_UPDATE
DEPENDENCIES = ['ambient_station']
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up an Ambient PWS sensor based on existing config."""
pass
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up an Ambient PWS sensor based on a config entry."""
ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
sensor_list = []
for mac_address, station in ambient.stations.items():
for condition in ambient.monitored_conditions:
name, unit = SENSOR_TYPES[condition]
sensor_list.append(
AmbientWeatherSensor(
ambient, mac_address, station[ATTR_NAME], condition, name,
unit))
async_add_entities(sensor_list, True)
class AmbientWeatherSensor(Entity):
"""Define an Ambient sensor."""
def __init__(
self, ambient, mac_address, station_name, sensor_type, sensor_name,
unit):
"""Initialize the sensor."""
self._ambient = ambient
self._async_unsub_dispatcher_connect = None
self._mac_address = mac_address
self._sensor_name = sensor_name
self._sensor_type = sensor_type
self._state = None
self._station_name = station_name
self._unit = unit
@property
def name(self):
"""Return the name of the sensor."""
return '{0}_{1}'.format(self._station_name, self._sensor_name)
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit
@property
def unique_id(self):
"""Return a unique, unchanging string that represents this sensor."""
return '{0}_{1}'.format(self._mac_address, self._sensor_name)
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def update():
"""Update the state."""
self.async_schedule_update_ha_state(True)
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
self.hass, TOPIC_UPDATE, update)
async def async_will_remove_from_hass(self):
"""Disconnect dispatcher listener when removed."""
if self._async_unsub_dispatcher_connect:
self._async_unsub_dispatcher_connect()
async def async_update(self):
"""Fetch new state data for the sensor."""
self._state = self._ambient.stations[
self._mac_address][ATTR_LAST_DATA].get(self._sensor_type)

View File

@ -0,0 +1,19 @@
{
"config": {
"title": "Ambient PWS",
"step": {
"user": {
"title": "Fill in your information",
"data": {
"api_key": "API Key",
"app_key": "Application Key"
}
}
},
"error": {
"identifier_exists": "Application Key and/or API Key already registered",
"invalid_key": "Invalid API Key and/or Application Key",
"no_devices": "No devices found in account"
}
}
}

View File

@ -123,8 +123,9 @@ ICON_MAP = {
'whitebalance_lock': 'mdi:white-balance-auto'
}
SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision',
'overlay', 'torch', 'whitebalance_lock', 'video_recording']
SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active',
'motion_detect', 'night_vision', 'overlay',
'torch', 'whitebalance_lock', 'video_recording']
SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
'battery_voltage', 'light', 'motion', 'pressure', 'proximity',

View File

@ -14,7 +14,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
REQUIREMENTS = ['aioasuswrt==1.1.18']
REQUIREMENTS = ['aioasuswrt==1.1.20']
_LOGGER = logging.getLogger(__name__)

View File

@ -9,7 +9,7 @@
},
"step": {
"init": {
"description": "Prosz\u0119 wybra\u0107 jedn\u0105 z us\u0142ugi powiadamiania:",
"description": "Prosz\u0119 wybra\u0107 jedn\u0105 us\u0142ug\u0119 powiadamiania:",
"title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144"
},
"setup": {

View File

@ -21,7 +21,7 @@
},
"totp": {
"error": {
"invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistenta to\u010dna."
"invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistanta to\u010dna."
},
"step": {
"init": {

View File

@ -3,6 +3,11 @@
"notify": {
"error": {
"invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437."
},
"step": {
"setup": {
"title": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f"
}
}
}
}

View File

@ -15,19 +15,23 @@ import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['mqtt']
CONF_ENCODING = 'encoding'
CONF_TOPIC = 'topic'
DEFAULT_ENCODING = 'utf-8'
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): mqtt.DOMAIN,
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
})
async def async_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
topic = config.get(CONF_TOPIC)
topic = config[CONF_TOPIC]
payload = config.get(CONF_PAYLOAD)
encoding = config[CONF_ENCODING] or None
@callback
def mqtt_automation_listener(msg_topic, msg_payload, qos):
@ -50,5 +54,5 @@ async def async_trigger(hass, config, action, automation_info):
})
remove = await mqtt.async_subscribe(
hass, topic, mqtt_automation_listener)
hass, topic, mqtt_automation_listener, encoding=encoding)
return remove

View File

@ -15,8 +15,6 @@ DEPENDENCIES = ['homematicip_cloud']
_LOGGER = logging.getLogger(__name__)
STATE_SMOKE_OFF = 'IDLE_OFF'
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
@ -65,7 +63,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
return True
if self._device.windowState is None:
return None
return self._device.windowState == WindowState.OPEN
return self._device.windowState != WindowState.CLOSED
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
@ -95,7 +93,9 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
@property
def is_on(self):
"""Return true if smoke is detected."""
return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF
from homematicip.base.enums import SmokeDetectorAlarmType
return (self._device.smokeDetectorAlarmType
!= SmokeDetectorAlarmType.IDLE_OFF)
class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice):

View File

@ -8,7 +8,6 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.maxcube import DATA_KEY
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)
@ -40,7 +39,7 @@ class MaxCubeShutter(BinarySensorDevice):
self._sensor_type = 'window'
self._rf_address = rf_address
self._cubehandle = handler
self._state = STATE_UNKNOWN
self._state = None
@property
def should_poll(self):

View File

@ -22,7 +22,7 @@ from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import async_track_state_change
from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.15.4']
REQUIREMENTS = ['numpy==1.16.0']
_LOGGER = logging.getLogger(__name__)

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['blinkpy==0.11.2']
REQUIREMENTS = ['blinkpy==0.12.1']
_LOGGER = logging.getLogger(__name__)

View File

@ -82,7 +82,7 @@ class AmcrestCam(Camera):
try:
return await async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
self._ffmpeg.ffmpeg_stream_content_type)
finally:
await stream.close()

View File

@ -104,7 +104,7 @@ class ArloCam(Camera):
try:
return await async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
self._ffmpeg.ffmpeg_stream_content_type)
finally:
await stream.close()

View File

@ -101,7 +101,7 @@ class CanaryCamera(Camera):
try:
return await async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
self._ffmpeg.ffmpeg_stream_content_type)
finally:
await stream.close()

View File

@ -68,7 +68,7 @@ class FFmpegCamera(Camera):
try:
return await async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
self._manager.ffmpeg_stream_content_type)
finally:
await stream.close()

View File

@ -213,7 +213,8 @@ class ONVIFHassCamera(Camera):
if not self._input:
return None
stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary,
ffmpeg_manager = self.hass.data[DATA_FFMPEG]
stream = CameraMjpeg(ffmpeg_manager.binary,
loop=self.hass.loop)
await stream.open_camera(
self._input, extra_cmd=self._ffmpeg_arguments)
@ -221,7 +222,7 @@ class ONVIFHassCamera(Camera):
try:
return await async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
ffmpeg_manager.ffmpeg_stream_content_type)
finally:
await stream.close()

View File

@ -142,7 +142,7 @@ class RingCam(Camera):
try:
return await async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
self._ffmpeg.ffmpeg_stream_content_type)
finally:
await stream.close()

View File

@ -161,6 +161,6 @@ class XiaomiCamera(Camera):
try:
return await async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
self._manager.ffmpeg_stream_content_type)
finally:
await stream.close()

View File

@ -147,6 +147,6 @@ class YiCamera(Camera):
try:
return await async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
self._manager.ffmpeg_stream_content_type)
finally:
await stream.close()

View File

@ -18,7 +18,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE,
STATE_ON, STATE_OFF, TEMP_CELSIUS, PRECISION_WHOLE,
PRECISION_TENTHS)
DEFAULT_MIN_TEMP = 7
@ -208,7 +208,7 @@ class ClimateDevice(Entity):
return self.current_operation
if self.is_on:
return STATE_ON
return STATE_UNKNOWN
return None
@property
def precision(self):

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
REQUIREMENTS = ['millheater==0.3.3']
REQUIREMENTS = ['millheater==0.3.4']
_LOGGER = logging.getLogger(__name__)

View File

@ -18,7 +18,7 @@ from homeassistant.components.climate import (
SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE)
from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT,
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['nest']
@ -163,7 +163,7 @@ class NestThermostat(ClimateDevice):
return self._mode
if self._mode == NEST_MODE_HEAT_COOL:
return STATE_AUTO
return STATE_UNKNOWN
return None
@property
def target_temperature(self):

View File

@ -219,6 +219,11 @@ class RadioThermostat(ClimateDevice):
"""Return true if away mode is on."""
return self._away
@property
def is_on(self):
"""Return true if on."""
return self._tstate != STATE_IDLE
def update(self):
"""Update and validate the data from the thermostat."""
# Radio thermostats are very slow, and sometimes don't respond

View File

@ -106,6 +106,7 @@ async def async_setup(hass, config):
)
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
await auth_api.async_setup(hass, cloud)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start)
await http_api.async_setup(hass)
return True
@ -263,7 +264,7 @@ class Cloud:
self.access_token = info['access_token']
self.refresh_token = info['refresh_token']
self.hass.add_job(self.iot.connect())
self.hass.async_create_task(self.iot.connect())
def _decode_claims(self, token): # pylint: disable=no-self-use
"""Decode the claims in a token."""

View File

@ -1,4 +1,10 @@
"""Package to communicate with the authentication API."""
import asyncio
import logging
import random
_LOGGER = logging.getLogger(__name__)
class CloudError(Exception):
@ -39,6 +45,40 @@ AWS_EXCEPTIONS = {
}
async def async_setup(hass, cloud):
"""Configure the auth api."""
refresh_task = None
async def handle_token_refresh():
"""Handle Cloud access token refresh."""
sleep_time = 5
sleep_time = random.randint(2400, 3600)
while True:
try:
await asyncio.sleep(sleep_time)
await hass.async_add_executor_job(renew_access_token, cloud)
except CloudError as err:
_LOGGER.error("Can't refresh cloud token: %s", err)
except asyncio.CancelledError:
# Task is canceled, stop it.
break
sleep_time = random.randint(3100, 3600)
async def on_connect():
"""When the instance is connected."""
nonlocal refresh_task
refresh_task = hass.async_create_task(handle_token_refresh())
async def on_disconnect():
"""When the instance is disconnected."""
nonlocal refresh_task
refresh_task.cancel()
cloud.iot.register_on_connect(on_connect)
cloud.iot.register_on_disconnect(on_disconnect)
def _map_aws_exception(err):
"""Map AWS exception to our exceptions."""
ex = AWS_EXCEPTIONS.get(err.response['Error']['Code'], UnknownError)
@ -47,7 +87,7 @@ def _map_aws_exception(err):
def register(cloud, email, password):
"""Register a new account."""
from botocore.exceptions import ClientError
from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(cloud)
# Workaround for bug in Warrant. PR with fix:
@ -55,13 +95,16 @@ def register(cloud, email, password):
cognito.add_base_attributes()
try:
cognito.register(email, password)
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def resend_email_confirm(cloud, email):
"""Resend email confirmation."""
from botocore.exceptions import ClientError
from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(cloud, username=email)
@ -72,18 +115,23 @@ def resend_email_confirm(cloud, email):
)
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def forgot_password(cloud, email):
"""Initialize forgotten password flow."""
from botocore.exceptions import ClientError
from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(cloud, username=email)
try:
cognito.initiate_forgot_password()
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def login(cloud, email, password):
@ -97,7 +145,7 @@ def login(cloud, email, password):
def check_token(cloud):
"""Check that the token is valid and verify if needed."""
from botocore.exceptions import ClientError
from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(
cloud,
@ -109,13 +157,17 @@ def check_token(cloud):
cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token
cloud.write_user_info()
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def renew_access_token(cloud):
"""Renew access token."""
from botocore.exceptions import ClientError
from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(
cloud,
@ -127,13 +179,17 @@ def renew_access_token(cloud):
cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token
cloud.write_user_info()
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def _authenticate(cloud, email, password):
"""Log in and return an authenticated Cognito instance."""
from botocore.exceptions import ClientError
from botocore.exceptions import ClientError, EndpointConnectionError
from warrant.exceptions import ForceChangePasswordException
assert not cloud.is_logged_in, 'Cannot login if already logged in.'
@ -145,11 +201,14 @@ def _authenticate(cloud, email, password):
return cognito
except ForceChangePasswordException:
raise PasswordChangeRequired
raise PasswordChangeRequired()
except ClientError as err:
raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def _cognito(cloud, **kwargs):
"""Get the client credentials."""

View File

@ -107,13 +107,16 @@ def _handle_cloud_errors(handler):
result = await handler(view, request, *args, **kwargs)
return result
except (auth_api.CloudError, asyncio.TimeoutError) as err:
except Exception as err: # pylint: disable=broad-except
err_info = _CLOUD_ERRORS.get(err.__class__)
if err_info is None:
_LOGGER.exception(
"Unexpected error processing request for %s", request.path)
err_info = (502, 'Unexpected error: {}'.format(err))
status, msg = err_info
return view.json_message(msg, status_code=status,
message_code=err.__class__.__name__)
return view.json_message(
msg, status_code=status,
message_code=err.__class__.__name__.lower())
return error_handler

View File

@ -12,9 +12,10 @@ from homeassistant.components.alexa import smart_home as alexa
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.core import callback
from homeassistant.util.decorator import Registry
from homeassistant.util.aiohttp import MockRequest, serialize_response
from homeassistant.util.aiohttp import MockRequest
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import auth_api
from . import utils
from .const import MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL
HANDLERS = Registry()
@ -61,12 +62,18 @@ class CloudIoT:
# Local code waiting for a response
self._response_handler = {}
self._on_connect = []
self._on_disconnect = []
@callback
def register_on_connect(self, on_connect_cb):
"""Register an async on_connect callback."""
self._on_connect.append(on_connect_cb)
@callback
def register_on_disconnect(self, on_disconnect_cb):
"""Register an async on_disconnect callback."""
self._on_disconnect.append(on_disconnect_cb)
@property
def connected(self):
"""Return if we're currently connected."""
@ -101,6 +108,17 @@ class CloudIoT:
# Still adding it here to make sure we can always reconnect
_LOGGER.exception("Unexpected error")
if self.state == STATE_CONNECTED and self._on_disconnect:
try:
yield from asyncio.wait([
cb() for cb in self._on_disconnect
])
except Exception: # pylint: disable=broad-except
# Safety net. This should never hit.
# Still adding it here to make sure we don't break the flow
_LOGGER.exception(
"Unexpected error in on_disconnect callbacks")
if self.close_requested:
break
@ -191,7 +209,13 @@ class CloudIoT:
self.state = STATE_CONNECTED
if self._on_connect:
yield from asyncio.wait([cb() for cb in self._on_connect])
try:
yield from asyncio.wait([cb() for cb in self._on_connect])
except Exception: # pylint: disable=broad-except
# Safety net. This should never hit.
# Still adding it here to make sure we don't break the flow
_LOGGER.exception(
"Unexpected error in on_connect callbacks")
while not client.closed:
msg = yield from client.receive()
@ -325,11 +349,6 @@ async def async_handle_cloud(hass, cloud, payload):
await cloud.logout()
_LOGGER.error("You have been logged out from Home Assistant cloud: %s",
payload['reason'])
elif action == 'refresh_auth':
# Refresh the auth token between now and payload['seconds']
hass.helpers.event.async_call_later(
random.randint(0, payload['seconds']),
lambda now: auth_api.check_token(cloud))
else:
_LOGGER.warning("Received unknown cloud action: %s", action)
@ -360,10 +379,8 @@ async def async_handle_webhook(hass, cloud, payload):
response = await hass.components.webhook.async_handle_webhook(
found['webhook_id'], request)
response_dict = serialize_response(response)
response_dict = utils.aiohttp_serialize_response(response)
body = response_dict.get('body')
if body:
body = body.decode('utf-8')
return {
'body': body,

View File

@ -0,0 +1,13 @@
"""Helper functions for cloud components."""
from typing import Any, Dict
from aiohttp import web
def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]:
"""Serialize an aiohttp response to a dictionary."""
return {
'status': response.status,
'body': response.text,
'headers': dict(response.headers),
}

View File

@ -14,6 +14,7 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = (
'area_registry',
'auth',
'auth_provider_homeassistant',
'automation',

View File

@ -0,0 +1,126 @@
"""HTTP views to interact with the area registry."""
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.websocket_api.decorators import (
async_response, require_admin)
from homeassistant.core import callback
from homeassistant.helpers.area_registry import async_get_registry
DEPENDENCIES = ['websocket_api']
WS_TYPE_LIST = 'config/area_registry/list'
SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_LIST,
})
WS_TYPE_CREATE = 'config/area_registry/create'
SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_CREATE,
vol.Required('name'): str,
})
WS_TYPE_DELETE = 'config/area_registry/delete'
SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_DELETE,
vol.Required('area_id'): str,
})
WS_TYPE_UPDATE = 'config/area_registry/update'
SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_UPDATE,
vol.Required('area_id'): str,
vol.Required('name'): str,
})
async def async_setup(hass):
"""Enable the Area Registry views."""
hass.components.websocket_api.async_register_command(
WS_TYPE_LIST, websocket_list_areas, SCHEMA_WS_LIST
)
hass.components.websocket_api.async_register_command(
WS_TYPE_CREATE, websocket_create_area, SCHEMA_WS_CREATE
)
hass.components.websocket_api.async_register_command(
WS_TYPE_DELETE, websocket_delete_area, SCHEMA_WS_DELETE
)
hass.components.websocket_api.async_register_command(
WS_TYPE_UPDATE, websocket_update_area, SCHEMA_WS_UPDATE
)
return True
@async_response
async def websocket_list_areas(hass, connection, msg):
"""Handle list areas command."""
registry = await async_get_registry(hass)
connection.send_message(websocket_api.result_message(
msg['id'], [{
'name': entry.name,
'area_id': entry.id,
} for entry in registry.async_list_areas()]
))
@require_admin
@async_response
async def websocket_create_area(hass, connection, msg):
"""Create area command."""
registry = await async_get_registry(hass)
try:
entry = registry.async_create(msg['name'])
except ValueError as err:
connection.send_message(websocket_api.error_message(
msg['id'], 'invalid_info', str(err)
))
else:
connection.send_message(websocket_api.result_message(
msg['id'], _entry_dict(entry)
))
@require_admin
@async_response
async def websocket_delete_area(hass, connection, msg):
"""Delete area command."""
registry = await async_get_registry(hass)
try:
await registry.async_delete(msg['area_id'])
except KeyError:
connection.send_message(websocket_api.error_message(
msg['id'], 'invalid_info', "Area ID doesn't exist"
))
else:
connection.send_message(websocket_api.result_message(
msg['id'], 'success'
))
@require_admin
@async_response
async def websocket_update_area(hass, connection, msg):
"""Handle update area websocket command."""
registry = await async_get_registry(hass)
try:
entry = registry.async_update(msg['area_id'], msg['name'])
except ValueError as err:
connection.send_message(websocket_api.error_message(
msg['id'], 'invalid_info', str(err)
))
else:
connection.send_message(websocket_api.result_message(
msg['id'], _entry_dict(entry)
))
@callback
def _entry_dict(entry):
"""Convert entry to API format."""
return {
'area_id': entry.id,
'name': entry.name
}

View File

@ -1,8 +1,11 @@
"""HTTP views to interact with the device registry."""
import voluptuous as vol
from homeassistant.helpers.device_registry import async_get_registry
from homeassistant.components import websocket_api
from homeassistant.components.websocket_api.decorators import (
async_response, require_admin)
from homeassistant.core import callback
from homeassistant.helpers.device_registry import async_get_registry
DEPENDENCIES = ['websocket_api']
@ -11,29 +14,60 @@ SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_LIST,
})
WS_TYPE_UPDATE = 'config/device_registry/update'
SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_UPDATE,
vol.Required('device_id'): str,
vol.Optional('area_id'): vol.Any(str, None),
})
async def async_setup(hass):
"""Enable the Entity Registry views."""
"""Enable the Device Registry views."""
hass.components.websocket_api.async_register_command(
WS_TYPE_LIST, websocket_list_devices,
SCHEMA_WS_LIST
)
hass.components.websocket_api.async_register_command(
WS_TYPE_UPDATE, websocket_update_device, SCHEMA_WS_UPDATE
)
return True
@websocket_api.async_response
@async_response
async def websocket_list_devices(hass, connection, msg):
"""Handle list devices command."""
registry = await async_get_registry(hass)
connection.send_message(websocket_api.result_message(
msg['id'], [{
'config_entries': list(entry.config_entries),
'connections': list(entry.connections),
'manufacturer': entry.manufacturer,
'model': entry.model,
'name': entry.name,
'sw_version': entry.sw_version,
'id': entry.id,
'hub_device_id': entry.hub_device_id,
} for entry in registry.devices.values()]
msg['id'], [_entry_dict(entry) for entry in registry.devices.values()]
))
@require_admin
@async_response
async def websocket_update_device(hass, connection, msg):
"""Handle update area websocket command."""
registry = await async_get_registry(hass)
entry = registry.async_update_device(
msg['device_id'], area_id=msg['area_id'])
connection.send_message(websocket_api.result_message(
msg['id'], _entry_dict(entry)
))
@callback
def _entry_dict(entry):
"""Convert entry to API format."""
return {
'config_entries': list(entry.config_entries),
'connections': list(entry.connections),
'manufacturer': entry.manufacturer,
'model': entry.model,
'name': entry.name,
'sw_version': entry.sw_version,
'id': entry.id,
'hub_device_id': entry.hub_device_id,
'area_id': entry.area_id,
}

View File

@ -5,7 +5,8 @@ from homeassistant.core import callback
from homeassistant.helpers.entity_registry import async_get_registry
from homeassistant.components import websocket_api
from homeassistant.components.websocket_api.const import ERR_NOT_FOUND
from homeassistant.components.websocket_api.decorators import async_response
from homeassistant.components.websocket_api.decorators import (
async_response, require_admin)
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['websocket_api']
@ -30,6 +31,12 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Optional('new_entity_id'): str,
})
WS_TYPE_REMOVE = 'config/entity_registry/remove'
SCHEMA_WS_REMOVE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_REMOVE,
vol.Required('entity_id'): cv.entity_id
})
async def async_setup(hass):
"""Enable the Entity Registry views."""
@ -45,6 +52,10 @@ async def async_setup(hass):
WS_TYPE_UPDATE, websocket_update_entity,
SCHEMA_WS_UPDATE
)
hass.components.websocket_api.async_register_command(
WS_TYPE_REMOVE, websocket_remove_entity,
SCHEMA_WS_REMOVE
)
return True
@ -56,14 +67,7 @@ async def websocket_list_entities(hass, connection, msg):
"""
registry = await async_get_registry(hass)
connection.send_message(websocket_api.result_message(
msg['id'], [{
'config_entry_id': entry.config_entry_id,
'device_id': entry.device_id,
'disabled_by': entry.disabled_by,
'entity_id': entry.entity_id,
'name': entry.name,
'platform': entry.platform,
} for entry in registry.entities.values()]
msg['id'], [_entry_dict(entry) for entry in registry.entities.values()]
))
@ -86,6 +90,7 @@ async def websocket_get_entity(hass, connection, msg):
))
@require_admin
@async_response
async def websocket_update_entity(hass, connection, msg):
"""Handle update entity websocket command.
@ -125,10 +130,32 @@ async def websocket_update_entity(hass, connection, msg):
))
@require_admin
@async_response
async def websocket_remove_entity(hass, connection, msg):
"""Handle remove entity websocket command.
Async friendly.
"""
registry = await async_get_registry(hass)
if msg['entity_id'] not in registry.entities:
connection.send_message(websocket_api.error_message(
msg['id'], ERR_NOT_FOUND, 'Entity not found'))
return
registry.async_remove(msg['entity_id'])
connection.send_message(websocket_api.result_message(msg['id']))
@callback
def _entry_dict(entry):
"""Convert entry to API format."""
return {
'config_entry_id': entry.config_entry_id,
'device_id': entry.device_id,
'disabled_by': entry.disabled_by,
'entity_id': entry.entity_id,
'name': entry.name
'name': entry.name,
'platform': entry.platform,
}

View File

@ -21,7 +21,7 @@ from homeassistant.const import (
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION, STATE_OPEN,
STATE_CLOSED, STATE_UNKNOWN, STATE_OPENING, STATE_CLOSING, ATTR_ENTITY_ID)
STATE_CLOSED, STATE_OPENING, STATE_CLOSING, ATTR_ENTITY_ID)
_LOGGER = logging.getLogger(__name__)
@ -178,7 +178,7 @@ class CoverDevice(Entity):
closed = self.is_closed
if closed is None:
return STATE_UNKNOWN
return None
return STATE_CLOSED if closed else STATE_OPEN

View File

@ -14,7 +14,7 @@ from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.const import (
CONF_DEVICE, CONF_USERNAME, CONF_PASSWORD, CONF_ACCESS_TOKEN, CONF_NAME,
STATE_UNKNOWN, STATE_CLOSED, STATE_OPEN, CONF_COVERS)
STATE_CLOSED, STATE_OPEN, CONF_COVERS)
_LOGGER = logging.getLogger(__name__)
@ -83,7 +83,7 @@ class GaradgetCover(CoverDevice):
self.obtained_token = False
self._username = args['username']
self._password = args['password']
self._state = STATE_UNKNOWN
self._state = None
self.time_in_state = None
self.signal = None
self.sensor = None
@ -156,7 +156,7 @@ class GaradgetCover(CoverDevice):
@property
def is_closed(self):
"""Return if the cover is closed."""
if self._state == STATE_UNKNOWN:
if self._state is None:
return None
return self._state == STATE_CLOSED
@ -226,7 +226,7 @@ class GaradgetCover(CoverDevice):
try:
status = self._get_variable('doorStatus')
_LOGGER.debug("Current Status: %s", status['status'])
self._state = STATES_MAP.get(status['status'], STATE_UNKNOWN)
self._state = STATES_MAP.get(status['status'], None)
self.time_in_state = status['time']
self.signal = status['signal']
self.sensor = status['sensor']

View File

@ -0,0 +1,70 @@
"""
Support for HomematicIP Cloud cover devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.homematicip_cloud/
"""
import logging
from homeassistant.components.cover import (
ATTR_POSITION, CoverDevice)
from homeassistant.components.homematicip_cloud import (
HMIPC_HAPID, HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN)
DEPENDENCIES = ['homematicip_cloud']
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up the HomematicIP Cloud cover devices."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the HomematicIP cover from a config entry."""
from homematicip.aio.device import AsyncFullFlushShutter
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
for device in home.devices:
if isinstance(device, AsyncFullFlushShutter):
devices.append(HomematicipCoverShutter(home, device))
if devices:
async_add_entities(devices)
class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice):
"""Representation of a HomematicIP Cloud cover device."""
@property
def current_cover_position(self):
"""Return current position of cover."""
return int(self._device.shutterLevel * 100)
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
position = kwargs[ATTR_POSITION]
level = position / 100.0
await self._device.set_shutter_level(level)
@property
def is_closed(self):
"""Return if the cover is closed."""
if self._device.shutterLevel is not None:
return self._device.shutterLevel == 0
return None
async def async_open_cover(self, **kwargs):
"""Open the cover."""
await self._device.set_shutter_level(1)
async def async_close_cover(self, **kwargs):
"""Close the cover."""
await self._device.set_shutter_level(0)
async def async_stop_cover(self, **kwargs):
"""Stop the device if in motion."""
await self._device.set_shutter_stop()

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
STATE_OPEN, STATE_OPENING)
from homeassistant.helpers import aiohttp_client, config_validation as cv
REQUIREMENTS = ['pymyq==1.0.0']
REQUIREMENTS = ['pymyq==1.1.0']
_LOGGER = logging.getLogger(__name__)
MYQ_TO_HASS = {

View File

@ -4,8 +4,7 @@ Support for Wink Covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.wink/
"""
from homeassistant.components.cover import CoverDevice, STATE_UNKNOWN, \
ATTR_POSITION
from homeassistant.components.cover import CoverDevice, ATTR_POSITION
from homeassistant.components.wink import WinkDevice, DOMAIN
DEPENDENCIES = ['wink']
@ -54,7 +53,7 @@ class WinkCoverDevice(WinkDevice, CoverDevice):
"""Return the current position of cover shutter."""
if self.wink.state() is not None:
return int(self.wink.state()*100)
return STATE_UNKNOWN
return None
@property
def is_closed(self):

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "\u8bbe\u5907\u5df2\u914d\u7f6e\u5b8c\u6210",
"device_fail": "\u521b\u5efa\u8bbe\u5907\u65f6\u51fa\u73b0\u610f\u5916\u9519\u8bef\u3002",
"device_timeout": "\u8fde\u63a5\u8bbe\u5907\u8d85\u65f6\u3002"
},
"step": {
"user": {
"data": {
"host": "\u4e3b\u673a"
},
"description": "\u8f93\u5165\u60a8\u7684 Daikin \u7a7a\u8c03\u7684IP\u5730\u5740\u3002",
"title": "\u914d\u7f6e Daikin \u7a7a\u8c03"
}
},
"title": "Daikin \u7a7a\u8c03"
}
}

View File

@ -0,0 +1,80 @@
"""
Support for Danfoss Air HRV.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/danfoss_air/
"""
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.const import CONF_HOST
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
REQUIREMENTS = ['pydanfossair==0.0.6']
_LOGGER = logging.getLogger(__name__)
DANFOSS_AIR_PLATFORMS = ['sensor', 'binary_sensor']
DOMAIN = 'danfoss_air'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Danfoss Air component."""
conf = config[DOMAIN]
hass.data[DOMAIN] = DanfossAir(conf[CONF_HOST])
for platform in DANFOSS_AIR_PLATFORMS:
discovery.load_platform(hass, platform, DOMAIN, {}, config)
return True
class DanfossAir:
"""Handle all communication with Danfoss Air CCM unit."""
def __init__(self, host):
"""Initialize the Danfoss Air CCM connection."""
self._data = {}
from pydanfossair.danfossclient import DanfossClient
self._client = DanfossClient(host)
def get_value(self, item):
"""Get value for sensor."""
return self._data.get(item)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Use the data from Danfoss Air API."""
_LOGGER.debug("Fetching data from Danfoss Air CCM module")
from pydanfossair.commands import ReadCommand
self._data[ReadCommand.exhaustTemperature] \
= self._client.command(ReadCommand.exhaustTemperature)
self._data[ReadCommand.outdoorTemperature] \
= self._client.command(ReadCommand.outdoorTemperature)
self._data[ReadCommand.supplyTemperature] \
= self._client.command(ReadCommand.supplyTemperature)
self._data[ReadCommand.extractTemperature] \
= self._client.command(ReadCommand.extractTemperature)
self._data[ReadCommand.humidity] \
= round(self._client.command(ReadCommand.humidity), 2)
self._data[ReadCommand.filterPercent] \
= round(self._client.command(ReadCommand.filterPercent), 2)
self._data[ReadCommand.bypass] \
= self._client.command(ReadCommand.bypass)
_LOGGER.debug("Done fetching data from Danfoss Air CCM module")

View File

@ -0,0 +1,56 @@
"""
Support for the for Danfoss Air HRV binary sensor platform.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.danfoss_air/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.danfoss_air import DOMAIN \
as DANFOSS_AIR_DOMAIN
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the available Danfoss Air sensors etc."""
from pydanfossair.commands import ReadCommand
data = hass.data[DANFOSS_AIR_DOMAIN]
sensors = [["Danfoss Air Bypass Active", ReadCommand.bypass]]
dev = []
for sensor in sensors:
dev.append(DanfossAirBinarySensor(data, sensor[0], sensor[1]))
add_entities(dev, True)
class DanfossAirBinarySensor(BinarySensorDevice):
"""Representation of a Danfoss Air binary sensor."""
def __init__(self, data, name, sensor_type):
"""Initialize the Danfoss Air binary sensor."""
self._data = data
self._name = name
self._state = None
self._type = sensor_type
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def is_on(self):
"""Return the state of the sensor."""
return self._state
@property
def device_class(self):
"""Type of device class."""
return "opening"
def update(self):
"""Fetch new state data for the sensor."""
self._data.update()
self._state = self._data.get_value(self._type)

View File

@ -0,0 +1,76 @@
"""
Support for the for Danfoss Air HRV sensor platform.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/sensor.danfoss_air/
"""
from homeassistant.components.danfoss_air import DOMAIN \
as DANFOSS_AIR_DOMAIN
from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the available Danfoss Air sensors etc."""
from pydanfossair.commands import ReadCommand
data = hass.data[DANFOSS_AIR_DOMAIN]
sensors = [
["Danfoss Air Exhaust Temperature", TEMP_CELSIUS,
ReadCommand.exhaustTemperature],
["Danfoss Air Outdoor Temperature", TEMP_CELSIUS,
ReadCommand.outdoorTemperature],
["Danfoss Air Supply Temperature", TEMP_CELSIUS,
ReadCommand.supplyTemperature],
["Danfoss Air Extract Temperature", TEMP_CELSIUS,
ReadCommand.extractTemperature],
["Danfoss Air Remaining Filter", '%',
ReadCommand.filterPercent],
["Danfoss Air Humidity", '%',
ReadCommand.humidity]
]
dev = []
for sensor in sensors:
dev.append(DanfossAir(data, sensor[0], sensor[1], sensor[2]))
add_entities(dev, True)
class DanfossAir(Entity):
"""Representation of a Sensor."""
def __init__(self, data, name, sensor_unit, sensor_type):
"""Initialize the sensor."""
self._data = data
self._name = name
self._state = None
self._type = sensor_type
self._unit = sensor_unit
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit
def update(self):
"""Update the new state of the sensor.
This is done through the DanfossAir object that does the actual
communication with the Air CCM.
"""
self._data.update()
self._state = self._data.get_value(self._type)

View File

@ -17,7 +17,7 @@
"title": "Define deCONZ gateway"
},
"link": {
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button",
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button",
"title": "Link with deCONZ"
},
"options": {

View File

@ -12,7 +12,7 @@
"init": {
"data": {
"host": "Host",
"port": "Port (warto\u015b\u0107 domy\u015blna: \"80\")"
"port": "Port"
},
"title": "Zdefiniuj bramk\u0119 deCONZ"
},
@ -28,6 +28,6 @@
"title": "Dodatkowe opcje konfiguracji dla deCONZ"
}
},
"title": "deCONZ"
"title": "Brama deCONZ Zigbee"
}
}

View File

@ -0,0 +1,107 @@
"""
Support for EE Brightbox router.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.ee_brightbox/
"""
import logging
import voluptuous as vol
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['eebrightbox==0.0.4']
_LOGGER = logging.getLogger(__name__)
CONF_VERSION = 'version'
CONF_DEFAULT_IP = '192.168.1.1'
CONF_DEFAULT_USERNAME = 'admin'
CONF_DEFAULT_VERSION = 2
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_VERSION, default=CONF_DEFAULT_VERSION): cv.positive_int,
vol.Required(CONF_HOST, default=CONF_DEFAULT_IP): cv.string,
vol.Required(CONF_USERNAME, default=CONF_DEFAULT_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
def get_scanner(hass, config):
"""Return a router scanner instance."""
scanner = EEBrightBoxScanner(config[DOMAIN])
return scanner if scanner.check_config() else None
class EEBrightBoxScanner(DeviceScanner):
"""Scan EE Brightbox router."""
def __init__(self, config):
"""Initialise the scanner."""
self.config = config
self.devices = {}
def check_config(self):
"""Check if provided configuration and credentials are correct."""
from eebrightbox import EEBrightBox, EEBrightBoxException
try:
with EEBrightBox(self.config) as ee_brightbox:
return bool(ee_brightbox.get_devices())
except EEBrightBoxException:
_LOGGER.exception("Failed to connect to the router")
return False
def scan_devices(self):
"""Scan for devices."""
from eebrightbox import EEBrightBox
with EEBrightBox(self.config) as ee_brightbox:
self.devices = {d['mac']: d for d in ee_brightbox.get_devices()}
macs = [d['mac'] for d in self.devices.values() if d['activity_ip']]
_LOGGER.debug('Scan devices %s', macs)
return macs
def get_device_name(self, device):
"""Get the name of a device from hostname."""
if device in self.devices:
return self.devices[device]['hostname'] or None
return None
def get_extra_attributes(self, device):
"""
Get the extra attributes of a device.
Extra attributes include:
- ip
- mac
- port - ethX or wifiX
- last_active
"""
port_map = {
'wl1': 'wifi5Ghz',
'wl0': 'wifi2.4Ghz',
'eth0': 'eth0',
'eth1': 'eth1',
'eth2': 'eth2',
'eth3': 'eth3',
}
if device in self.devices:
return {
'ip': self.devices[device]['ip'],
'mac': self.devices[device]['mac'],
'port': port_map[self.devices[device]['port']],
'last_active': self.devices[device]['time_last_active'],
}
return {}

View File

@ -1,32 +0,0 @@
"""
Support for the GPSLogger platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.gpslogger/
"""
import logging
from homeassistant.components.gpslogger import TRACKER_UPDATE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['gpslogger']
async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType,
async_see, discovery_info=None):
"""Set up an endpoint for the GPSLogger device tracker."""
async def _set_location(device, gps_location, battery, accuracy, attrs):
"""Fire HA event to set location."""
await async_see(
dev_id=device,
gps=gps_location,
battery=battery,
gps_accuracy=accuracy,
attributes=attrs
)
async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location)
return True

View File

@ -1,28 +0,0 @@
"""
Support for the Locative platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.locative/
"""
import logging
from homeassistant.components.locative import TRACKER_UPDATE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['locative']
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an endpoint for the Locative device tracker."""
async def _set_location(device, gps_location, location_name):
"""Fire HA event to set location."""
await async_see(
dev_id=device,
gps=gps_location,
location_name=location_name
)
async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location)
return True

View File

@ -0,0 +1,100 @@
"""Device tracker for Synology SRM routers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.synology_srm/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import (
CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
CONF_PORT, CONF_SSL, CONF_VERIFY_SSL)
REQUIREMENTS = ['synology-srm==0.0.3']
_LOGGER = logging.getLogger(__name__)
DEFAULT_USERNAME = 'admin'
DEFAULT_PORT = 8001
DEFAULT_SSL = True
DEFAULT_VERIFY_SSL = False
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
})
def get_scanner(hass, config):
"""Validate the configuration and return Synology SRM scanner."""
scanner = SynologySrmDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class SynologySrmDeviceScanner(DeviceScanner):
"""This class scans for devices connected to a Synology SRM router."""
def __init__(self, config):
"""Initialize the scanner."""
import synology_srm
self.client = synology_srm.Client(
host=config[CONF_HOST],
port=config[CONF_PORT],
username=config[CONF_USERNAME],
password=config[CONF_PASSWORD],
https=config[CONF_SSL]
)
if not config[CONF_VERIFY_SSL]:
self.client.http.disable_https_verify()
self.last_results = []
self.success_init = self._update_info()
_LOGGER.info("Synology SRM scanner initialized")
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return [device['mac'] for device in self.last_results]
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
filter_named = [result['hostname'] for result in self.last_results if
result['mac'] == device]
if filter_named:
return filter_named[0]
return None
def _update_info(self):
"""Check the router for connected devices."""
_LOGGER.debug("Scanning for connected devices")
devices = self.client.mesh.network_wifidevice()
last_results = []
for device in devices:
last_results.append({
'mac': device['mac'],
'hostname': device['hostname']
})
_LOGGER.debug(
"Found %d device(s) connected to the router",
len(devices)
)
self.last_results = last_results
return True

View File

@ -1,11 +1,11 @@
{
"config": {
"abort": {
"not_internet_accessible": " \u010ce \u017eelite prejemati sporo\u010dila dialogflow, mora biti Home Assistent dostopen prek interneta.",
"not_internet_accessible": "\u010ce \u017eelite prejemati sporo\u010dila dialogflow, mora biti Home Assistant dostopen prek interneta.",
"one_instance_allowed": "Potrebna je samo ena instanca."
},
"create_entry": {
"default": "Za po\u0161iljanje dogodkov Home Assistent-u, boste morali nastaviti [webhook z dialogflow]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) za nadaljna navodila."
"default": "Za po\u0161iljanje dogodkov Home Assistant-u, boste morali nastaviti [webhook z dialogflow]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) za nadaljna navodila."
},
"step": {
"user": {

View File

@ -1,10 +1,15 @@
{
"config": {
"abort": {
"not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u63a5\u5165\u4e92\u8054\u7f51\u4ee5\u63a5\u6536 Dialogflow \u6d88\u606f\u3002",
"one_instance_allowed": "\u4ec5\u9700\u4e00\u4e2a\u5b9e\u4f8b"
},
"create_entry": {
"default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u914d\u7f6e [Dialogflow \u7684 Webhook \u96c6\u6210]({dialogflow_url})\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u591a\u4fe1\u606f\u3002"
},
"step": {
"user": {
"description": "\u60a8\u786e\u5b9a\u8981\u8bbe\u7f6e Dialogflow \u5417?",
"title": "\u8bbe\u7f6e Dialogflow Webhook"
}
},

View File

@ -0,0 +1,79 @@
"""
Support for Dovado router.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/dovado/
"""
import logging
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT,
DEVICE_DEFAULT_NAME)
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['dovado==0.4.1']
DOMAIN = 'dovado'
CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_PORT): cv.port,
})
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
def setup(hass, config):
"""Set up the Dovado component."""
import dovado
hass.data[DOMAIN] = DovadoData(
dovado.Dovado(
config[CONF_USERNAME],
config[CONF_PASSWORD],
config.get(CONF_HOST),
config.get(CONF_PORT)
)
)
return True
class DovadoData:
"""Maintains a connection to the router."""
def __init__(self, client):
"""Set up a new Dovado connection."""
self._client = client
self.state = {}
@property
def name(self):
"""Name of the router."""
return self.state.get("product name", DEVICE_DEFAULT_NAME)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update device state."""
try:
self.state = self._client.state or {}
if not self.state:
return False
self.state.update(
connected=self.state.get("modem status") == "CONNECTED")
_LOGGER.debug("Received: %s", self.state)
return True
except OSError as error:
_LOGGER.warning("Could not contact the router: %s", error)
@property
def client(self):
"""Dovado client instance."""
return self._client

View File

@ -0,0 +1,38 @@
"""
Support for SMS notifications from the Dovado router.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.dovado/
"""
import logging
from homeassistant.components.dovado import DOMAIN as DOVADO_DOMAIN
from homeassistant.components.notify import BaseNotificationService, \
ATTR_TARGET
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['dovado']
def get_service(hass, config, discovery_info=None):
"""Get the Dovado Router SMS notification service."""
return DovadoSMSNotificationService(hass.data[DOVADO_DOMAIN].client)
class DovadoSMSNotificationService(BaseNotificationService):
"""Implement the notification service for the Dovado SMS component."""
def __init__(self, client):
"""Initialize the service."""
self._client = client
def send_message(self, message, **kwargs):
"""Send SMS to the specified target phone number."""
target = kwargs.get(ATTR_TARGET)
if not target:
_LOGGER.error("One target is required")
return
self._client.send_sms(target, message)

View File

@ -0,0 +1,116 @@
"""
Support for sensors from the Dovado router.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.dovado/
"""
import logging
import re
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.dovado import DOMAIN as DOVADO_DOMAIN
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_SENSORS
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['dovado']
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
SENSOR_UPLOAD = 'upload'
SENSOR_DOWNLOAD = 'download'
SENSOR_SIGNAL = 'signal'
SENSOR_NETWORK = 'network'
SENSOR_SMS_UNREAD = 'sms'
SENSORS = {
SENSOR_NETWORK: ('signal strength', 'Network', None,
'mdi:access-point-network'),
SENSOR_SIGNAL: ('signal strength', 'Signal Strength', '%',
'mdi:signal'),
SENSOR_SMS_UNREAD: ('sms unread', 'SMS unread', '',
'mdi:message-text-outline'),
SENSOR_UPLOAD: ('traffic modem tx', 'Sent', 'GB',
'mdi:cloud-upload'),
SENSOR_DOWNLOAD: ('traffic modem rx', 'Received', 'GB',
'mdi:cloud-download'),
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENSORS): vol.All(
cv.ensure_list, [vol.In(SENSORS)]
),
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Dovado sensor platform."""
dovado = hass.data[DOVADO_DOMAIN]
entities = []
for sensor in config[CONF_SENSORS]:
entities.append(DovadoSensor(dovado, sensor))
add_entities(entities)
class DovadoSensor(Entity):
"""Representation of a Dovado sensor."""
def __init__(self, data, sensor):
"""Initialize the sensor."""
self._data = data
self._sensor = sensor
self._state = self._compute_state()
def _compute_state(self):
state = self._data.state.get(SENSORS[self._sensor][0])
if self._sensor == SENSOR_NETWORK:
match = re.search(r"\((.+)\)", state)
return match.group(1) if match else None
if self._sensor == SENSOR_SIGNAL:
try:
return int(state.split()[0])
except ValueError:
return None
if self._sensor == SENSOR_SMS_UNREAD:
return int(state)
if self._sensor in [SENSOR_UPLOAD, SENSOR_DOWNLOAD]:
return round(float(state) / 1e6, 1)
return state
def update(self):
"""Update sensor values."""
self._data.update()
self._state = self._compute_state()
@property
def name(self):
"""Return the name of the sensor."""
return "{} {}".format(self._data.name, SENSORS[self._sensor][1])
@property
def state(self):
"""Return the sensor state."""
return self._state
@property
def icon(self):
"""Return the icon for the sensor."""
return SENSORS[self._sensor][3]
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return SENSORS[self._sensor][2]
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {k: v for k, v in self._data.state.items()
if k not in ['date', 'time']}

View File

@ -0,0 +1,98 @@
"""
Component to control ecoal/esterownik.pl coal/wood boiler controller.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ecoal_boiler/
"""
import logging
import voluptuous as vol
from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
CONF_MONITORED_CONDITIONS, CONF_SENSORS,
CONF_SWITCHES)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
REQUIREMENTS = ['ecoaliface==0.4.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "ecoal_boiler"
DATA_ECOAL_BOILER = 'data_' + DOMAIN
DEFAULT_USERNAME = "admin"
DEFAULT_PASSWORD = "admin"
# Available pump ids with assigned HA names
# Available as switches
AVAILABLE_PUMPS = {
"central_heating_pump": "Central heating pump",
"central_heating_pump2": "Central heating pump2",
"domestic_hot_water_pump": "Domestic hot water pump",
}
# Available temp sensor ids with assigned HA names
# Available as sensors
AVAILABLE_SENSORS = {
"outdoor_temp": 'Outdoor temperature',
"indoor_temp": 'Indoor temperature',
"indoor2_temp": 'Indoor temperature 2',
"domestic_hot_water_temp": 'Domestic hot water temperature',
"target_domestic_hot_water_temp": 'Target hot water temperature',
"feedwater_in_temp": 'Feedwater input temperature',
"feedwater_out_temp": 'Feedwater output temperature',
"target_feedwater_temp": 'Target feedwater temperature',
"fuel_feeder_temp": 'Fuel feeder temperature',
"exhaust_temp": 'Exhaust temperature',
}
SWITCH_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_PUMPS)):
vol.All(cv.ensure_list, [vol.In(AVAILABLE_PUMPS)])
})
SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(AVAILABLE_SENSORS)):
vol.All(cv.ensure_list, [vol.In(AVAILABLE_SENSORS)])
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_USERNAME,
default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD,
default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA,
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
})
}, extra=vol.ALLOW_EXTRA)
def setup(hass, hass_config):
"""Set up global ECoalController instance same for sensors and switches."""
from ecoaliface.simple import ECoalController
conf = hass_config[DOMAIN]
host = conf[CONF_HOST]
username = conf[CONF_USERNAME]
passwd = conf[CONF_PASSWORD]
# Creating ECoalController instance makes HTTP request to controller.
ecoal_contr = ECoalController(host, username, passwd)
if ecoal_contr.version is None:
# Wrong credentials nor network config
_LOGGER.error("Unable to read controller status from %s@%s"
" (wrong host/credentials)", username, host, )
return False
_LOGGER.debug("Detected controller version: %r @%s",
ecoal_contr.version, host, )
hass.data[DATA_ECOAL_BOILER] = ecoal_contr
# Setup switches
switches = conf[CONF_SWITCHES][CONF_MONITORED_CONDITIONS]
load_platform(hass, 'switch', DOMAIN, switches, hass_config)
# Setup temp sensors
sensors = conf[CONF_SENSORS][CONF_MONITORED_CONDITIONS]
load_platform(hass, 'sensor', DOMAIN, sensors, hass_config)
return True

View File

@ -22,7 +22,7 @@ from homeassistant.components.http import real_ip
from .hue_api import (
HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
HueOneLightChangeView, HueGroupView)
HueOneLightChangeView, HueGroupView, HueAllGroupsStateView)
from .upnp import DescriptionXmlView, UPNPResponderThread
DOMAIN = 'emulated_hue'
@ -105,6 +105,7 @@ async def async_setup(hass, yaml_config):
HueAllLightsStateView(config).register(app, app.router)
HueOneLightStateView(config).register(app, app.router)
HueOneLightChangeView(config).register(app, app.router)
HueAllGroupsStateView(config).register(app, app.router)
HueGroupView(config).register(app, app.router)
upnp_listener = UPNPResponderThread(

View File

@ -56,6 +56,28 @@ class HueUsernameView(HomeAssistantView):
return self.json([{'success': {'username': '12345678901234567890'}}])
class HueAllGroupsStateView(HomeAssistantView):
"""Group handler."""
url = '/api/{username}/groups'
name = 'emulated_hue:all_groups:state'
requires_auth = False
def __init__(self, config):
"""Initialize the instance of the view."""
self.config = config
@core.callback
def get(self, request, username):
"""Process a request to make the Brilliant Lightpad work."""
if not is_local(request[KEY_REAL_IP]):
return self.json_message('only local IPs allowed',
HTTP_BAD_REQUEST)
return self.json({
})
class HueGroupView(HomeAssistantView):
"""Group handler to get Logitech Pop working."""

View File

@ -0,0 +1,17 @@
{
"config": {
"abort": {
"name_exists": "El nombre ya existe"
},
"step": {
"user": {
"data": {
"host_ip": "IP del host",
"listen_port": "Puerto de escucha",
"name": "Nombre"
},
"title": "Definir la configuraci\u00f3n del servidor"
}
}
}
}

View File

@ -11,7 +11,7 @@
"host_ip": "\ud638\uc2a4\ud2b8 IP",
"listen_port": "\uc218\uc2e0 \ud3ec\ud2b8",
"name": "\uc774\ub984",
"upnp_bind_multicast": "\uba40\ud2f0 \uce90\uc2a4\ud2b8 \ubc14\uc778\ub4dc (\ucc38/\uac70\uc9d3)"
"upnp_bind_multicast": "\uba40\ud2f0 \uce90\uc2a4\ud2b8 \ud560\ub2f9 (\ucc38/\uac70\uc9d3)"
},
"title": "\uc11c\ubc84 \uad6c\uc131 \uc815\uc758"
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"name_exists": "Numm g\u00ebtt et schonn"
},
"step": {
"user": {
"data": {
"advertise_ip": "IP annonc\u00e9ieren",
"advertise_port": "Port annonc\u00e9ieren",
"host_ip": "IP vum Apparat",
"listen_port": "Port lauschteren",
"name": "Numm",
"upnp_bind_multicast": "Multicast abannen (Richteg/Falsch)"
},
"title": "Server Konfiguratioun d\u00e9fin\u00e9ieren"
}
},
"title": "EmulatedRoku"
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"name_exists": "Nazwa ju\u017c istnieje"
},
"step": {
"user": {
"data": {
"advertise_ip": "IP rozg\u0142aszania",
"advertise_port": "Port rozg\u0142aszania",
"host_ip": "IP hosta",
"listen_port": "Port nas\u0142uchu",
"name": "Nazwa",
"upnp_bind_multicast": "Powi\u0105\u017c multicast (prawda/fa\u0142sz)"
},
"title": "Zdefiniuj konfiguracj\u0119 serwera"
}
},
"title": "EmulatedRoku"
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"name_exists": "Ime \u017ee obstaja"
},
"step": {
"user": {
"data": {
"advertise_ip": "Advertise IP",
"advertise_port": "Advertise port",
"host_ip": "IP gostitelja",
"listen_port": "Vrata naprave",
"name": "Ime",
"upnp_bind_multicast": "Vezava multicasta (True / False)"
},
"title": "Dolo\u010dite konfiguracijo stre\u017enika"
}
},
"title": "EmulatedRoku"
}
}

View File

@ -0,0 +1,17 @@
{
"config": {
"abort": {
"name_exists": "\u540d\u79f0\u5df2\u5b58\u5728"
},
"step": {
"user": {
"data": {
"host_ip": "\u4e3b\u673aIP",
"listen_port": "\u76d1\u542c\u7aef\u53e3",
"name": "\u59d3\u540d"
},
"title": "\u5b9a\u4e49\u670d\u52a1\u5668\u914d\u7f6e"
}
}
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728"
},
"step": {
"user": {
"data": {
"advertise_ip": "\u5ee3\u64ad\u901a\u8a0a\u57e0",
"advertise_port": "\u5ee3\u64ad\u901a\u8a0a\u57e0",
"host_ip": "\u4e3b\u6a5f IP",
"listen_port": "\u76e3\u807d\u901a\u8a0a\u57e0",
"name": "\u540d\u7a31",
"upnp_bind_multicast": "\u7d81\u5b9a\u7fa4\u64ad\uff08Multicast\uff09True/False"
},
"title": "\u5b9a\u7fa9\u4f3a\u670d\u5668\u8a2d\u5b9a"
}
},
"title": "EmulatedRoku"
}
}

View File

@ -16,7 +16,7 @@ from .const import (
CONF_ADVERTISE_IP, CONF_ADVERTISE_PORT, CONF_HOST_IP, CONF_LISTEN_PORT,
CONF_SERVERS, CONF_UPNP_BIND_MULTICAST, DOMAIN)
REQUIREMENTS = ['emulated_roku==0.1.7']
REQUIREMENTS = ['emulated_roku==0.1.8']
SERVER_CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,

View File

@ -146,19 +146,19 @@ async def async_setup(hass, config):
@callback
def zones_updated_callback(data):
"""Handle zone timer updates."""
_LOGGER.info("Envisalink sent a zone update event. Updating zones...")
_LOGGER.debug("Envisalink sent a zone update event. Updating zones...")
async_dispatcher_send(hass, SIGNAL_ZONE_UPDATE, data)
@callback
def alarm_data_updated_callback(data):
"""Handle non-alarm based info updates."""
_LOGGER.info("Envisalink sent new alarm info. Updating alarms...")
_LOGGER.debug("Envisalink sent new alarm info. Updating alarms...")
async_dispatcher_send(hass, SIGNAL_KEYPAD_UPDATE, data)
@callback
def partition_updated_callback(data):
"""Handle partition changes thrown by evl (including alarms)."""
_LOGGER.info("The envisalink sent a partition update event")
_LOGGER.debug("The envisalink sent a partition update event")
async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data)
@callback

View File

@ -0,0 +1,25 @@
{
"config": {
"abort": {
"already_configured": "ESP ya est\u00e1 configurado"
},
"error": {
"invalid_password": "\u00a1Contrase\u00f1a incorrecta!"
},
"step": {
"authenticate": {
"data": {
"password": "Contrase\u00f1a"
},
"description": "Escribe la contrase\u00f1a que hayas establecido en tu configuraci\u00f3n.",
"title": "Escribe la contrase\u00f1a"
},
"user": {
"data": {
"host": "Host",
"port": "Puerto"
}
}
}
}
}

View File

@ -4,7 +4,7 @@
"already_configured": "ESP \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"error": {
"connection_error": "ESP \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. YAML \ud30c\uc77c\uc5d0 'api :' \ub97c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
"connection_error": "ESP \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. YAML \ud30c\uc77c\uc5d0 'api:' \ub97c \uad6c\uc131\ud588\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.",
"invalid_password": "\uc798\ubabb\ub41c \ube44\ubc00\ubc88\ud638",
"resolve_error": "ESP \uc758 \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uace0\uc815 IP \uc8fc\uc18c (https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694"
},

View File

@ -1,10 +1,10 @@
{
"config": {
"abort": {
"already_configured": "ESP jest ju\u017c skonfigurowany"
"already_configured": "ESP jest ju\u017c skonfigurowane"
},
"error": {
"connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 \"api:\".",
"connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.",
"invalid_password": "Nieprawid\u0142owe has\u0142o!",
"resolve_error": "Nie mo\u017cna rozpozna\u0107 adresu ESP. Je\u015bli ten b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, nale\u017cy ustawi\u0107 statyczny adres IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
@ -21,7 +21,7 @@
"host": "Host",
"port": "Port"
},
"description": "Wprowad\u017a ustawienia po\u0142\u0105czenia swojego [ESPHome] (https://esphomelib.com/) w\u0119z\u0142a.",
"description": "Wprowad\u017a ustawienia po\u0142\u0105czenia swojego [ESPHome](https://esphomelib.com/) w\u0119z\u0142a.",
"title": "ESPHome"
}
},

View File

@ -0,0 +1,27 @@
{
"config": {
"error": {
"connection_error": "\u65e0\u6cd5\u8fde\u63a5\u5230ESP\u3002\u8bf7\u786e\u4fdd\u60a8\u7684YAML\u6587\u4ef6\u5305\u542b'api:'\u884c\u3002",
"invalid_password": "\u65e0\u6548\u7684\u5bc6\u7801\uff01",
"resolve_error": "\u65e0\u6cd5\u89e3\u6790ESP\u7684\u5730\u5740\u3002\u5982\u679c\u6b64\u9519\u8bef\u4ecd\u7136\u5b58\u5728\uff0c\u8bf7\u8bbe\u7f6e\u9759\u6001IP\u5730\u5740\uff1ahttps://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
"step": {
"authenticate": {
"data": {
"password": "\u5bc6\u7801"
},
"description": "\u8bf7\u8f93\u5165\u60a8\u5728\u914d\u7f6e\u4e2d\u8bbe\u7f6e\u7684\u5bc6\u7801\u3002",
"title": "\u8f93\u5165\u5bc6\u7801"
},
"user": {
"data": {
"host": "\u4e3b\u673a",
"port": "\u7aef\u53e3"
},
"description": "\u8bf7\u8f93\u5165\u60a8\u7684 [ESPHome](https://esphomelib.com/) \u8282\u70b9\u7684\u8fde\u63a5\u8bbe\u7f6e\u3002",
"title": "ESPHome"
}
},
"title": "ESPHome"
}
}

View File

@ -12,8 +12,7 @@ import voluptuous as vol
from homeassistant.components import group
from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE,
SERVICE_TURN_OFF, ATTR_ENTITY_ID,
STATE_UNKNOWN)
SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.loader import bind_hass
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
@ -94,7 +93,7 @@ def is_on(hass, entity_id: str = None) -> bool:
"""Return if the fans are on based on the statemachine."""
entity_id = entity_id or ENTITY_ID_ALL_FANS
state = hass.states.get(entity_id)
return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN]
return state.attributes[ATTR_SPEED] not in [SPEED_OFF, None]
async def async_setup(hass, config: dict):
@ -199,7 +198,7 @@ class FanEntity(ToggleEntity):
@property
def is_on(self):
"""Return true if the entity is on."""
return self.speed not in [SPEED_OFF, STATE_UNKNOWN]
return self.speed not in [SPEED_OFF, None]
@property
def speed(self) -> str:

View File

@ -11,7 +11,6 @@ from homeassistant.components.comfoconnect import (
from homeassistant.components.fan import (
FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED)
from homeassistant.const import STATE_UNKNOWN
from homeassistant.helpers.dispatcher import (dispatcher_connect)
_LOGGER = logging.getLogger(__name__)
@ -79,7 +78,7 @@ class ComfoConnectFan(FanEntity):
speed = self._ccb.data[SENSOR_FAN_SPEED_MODE]
return SPEED_MAPPING[speed]
except KeyError:
return STATE_UNKNOWN
return None
@property
def speed_list(self):

View File

@ -7,7 +7,7 @@ https://home-assistant.io/components/fan.wink/
import logging
from homeassistant.components.fan import (
SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, STATE_UNKNOWN, SUPPORT_DIRECTION,
SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SUPPORT_DIRECTION,
SUPPORT_SET_SPEED, FanEntity)
from homeassistant.components.wink import DOMAIN, WinkDevice
@ -71,7 +71,7 @@ class WinkFanDevice(WinkDevice, FanEntity):
return SPEED_MEDIUM
if SPEED_HIGH == current_wink_speed:
return SPEED_HIGH
return STATE_UNKNOWN
return None
@property
def current_direction(self):

View File

@ -38,7 +38,7 @@ MODEL_AIRPURIFIER_MA1 = 'zhimi.airpurifier.ma1'
MODEL_AIRPURIFIER_MA2 = 'zhimi.airpurifier.ma2'
MODEL_AIRPURIFIER_SA1 = 'zhimi.airpurifier.sa1'
MODEL_AIRPURIFIER_SA2 = 'zhimi.airpurifier.sa2'
MODEL_AIRPURIFIER_MC1 = 'zhimi.airpurifier.mc1'
MODEL_AIRPURIFIER_2S = 'zhimi.airpurifier.mc1'
MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1'
MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1'
@ -62,7 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
MODEL_AIRPURIFIER_MA2,
MODEL_AIRPURIFIER_SA1,
MODEL_AIRPURIFIER_SA2,
MODEL_AIRPURIFIER_MC1,
MODEL_AIRPURIFIER_2S,
MODEL_AIRHUMIDIFIER_V1,
MODEL_AIRHUMIDIFIER_CA,
MODEL_AIRFRESH_VA2,
@ -175,6 +175,15 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 = {
ATTR_VOLUME: 'volume',
}
AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S = {
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
ATTR_BUZZER: 'buzzer',
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
ATTR_FILTER_TYPE: 'filter_type',
ATTR_ILLUMINANCE: 'illuminance',
}
AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = {
# Common set isn't used here. It's a very basic version of the device.
ATTR_AIR_QUALITY_INDEX: 'aqi',
@ -249,6 +258,7 @@ AVAILABLE_ATTRIBUTES_AIRFRESH = {
OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle']
OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite']
OPERATION_MODES_AIRPURIFIER_PRO_V7 = OPERATION_MODES_AIRPURIFIER_PRO
OPERATION_MODES_AIRPURIFIER_2S = ['Auto', 'Silent', 'Favorite']
OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle',
'Medium', 'High', 'Strong']
OPERATION_MODES_AIRFRESH = ['Auto', 'Silent', 'Interval', 'Low',
@ -289,6 +299,11 @@ FEATURE_FLAGS_AIRPURIFIER_PRO_V7 = (FEATURE_SET_CHILD_LOCK |
FEATURE_SET_FAVORITE_LEVEL |
FEATURE_SET_VOLUME)
FEATURE_FLAGS_AIRPURIFIER_2S = (FEATURE_SET_BUZZER |
FEATURE_SET_CHILD_LOCK |
FEATURE_SET_LED |
FEATURE_SET_FAVORITE_LEVEL)
FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_SET_BUZZER |
FEATURE_SET_CHILD_LOCK |
FEATURE_SET_LED)
@ -619,6 +634,10 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
self._available_attributes = \
AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7
self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO_V7
elif self._model == MODEL_AIRPURIFIER_2S:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_2S
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_2S
self._speed_list = OPERATION_MODES_AIRPURIFIER_2S
elif self._model == MODEL_AIRPURIFIER_V3:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3

View File

@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ffmpeg/
"""
import logging
import re
import voluptuous as vol
@ -16,7 +17,7 @@ from homeassistant.helpers.dispatcher import (
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['ha-ffmpeg==1.9']
REQUIREMENTS = ['ha-ffmpeg==1.11']
DOMAIN = 'ffmpeg'
@ -60,6 +61,8 @@ async def async_setup(hass, config):
conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY)
)
await manager.async_get_version()
# Register service
async def async_service_handle(service):
"""Handle service ffmpeg process."""
@ -96,12 +99,37 @@ class FFmpegManager:
self.hass = hass
self._cache = {}
self._bin = ffmpeg_bin
self._version = None
self._major_version = None
@property
def binary(self):
"""Return ffmpeg binary from config."""
return self._bin
async def async_get_version(self):
"""Return ffmpeg version."""
from haffmpeg.tools import FFVersion
ffversion = FFVersion(self._bin, self.hass.loop)
self._version = await ffversion.get_version()
self._major_version = None
if self._version is not None:
result = re.search(r"(\d+)\.", self._version)
if result is not None:
self._major_version = int(result.group(1))
return self._version, self._major_version
@property
def ffmpeg_stream_content_type(self):
"""Return HTTP content type for ffmpeg stream."""
if self._major_version is not None and self._major_version > 3:
return 'multipart/x-mixed-replace;boundary=ffmpeg'
return 'multipart/x-mixed-replace;boundary=ffserver'
class FFmpegBase(Entity):
"""Interface object for FFmpeg."""

View File

@ -12,7 +12,8 @@ import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN)
from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN,
CONF_UPDATE_INTERVAL)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@ -24,8 +25,6 @@ DEFAULT_INTERVAL = timedelta(minutes=10)
TIMEOUT = 10
UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php'
CONF_UPDATE_INTERVAL = 'update_interval'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Exclusive(CONF_URL, DOMAIN): cv.string,

View File

@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyfritzhome==0.4.0']
SUPPORTED_DOMAINS = ['binary_sensor', 'climate', 'switch']
SUPPORTED_DOMAINS = ['binary_sensor', 'climate', 'switch', 'sensor']
DOMAIN = 'fritzbox'

View File

@ -24,7 +24,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20190121.1']
REQUIREMENTS = ['home-assistant-frontend==20190203.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',

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