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/*/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
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/*/digital_ocean.py homeassistant/components/*/digital_ocean.py
homeassistant/components/danfoss_air/*
homeassistant/components/dominos.py homeassistant/components/dominos.py
homeassistant/components/doorbird.py homeassistant/components/doorbird.py
homeassistant/components/*/doorbird.py homeassistant/components/*/doorbird.py
homeassistant/components/dovado/*
homeassistant/components/dweet.py homeassistant/components/dweet.py
homeassistant/components/*/dweet.py homeassistant/components/*/dweet.py
homeassistant/components/eight_sleep.py homeassistant/components/eight_sleep.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
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/*/hlk_sw16.py homeassistant/components/*/hlk_sw16.py
homeassistant/components/homekit_controller/__init__.py homeassistant/components/homekit_controller/*
homeassistant/components/*/homekit_controller.py
homeassistant/components/homematic/__init__.py homeassistant/components/homematic/__init__.py
homeassistant/components/*/homematic.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/*/homematicip_cloud.py
homeassistant/components/homeworks.py homeassistant/components/homeworks.py
@ -385,6 +395,9 @@ omit =
homeassistant/components/tradfri.py homeassistant/components/tradfri.py
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_sms.py
homeassistant/components/notify/twilio_call.py homeassistant/components/notify/twilio_call.py
@ -443,15 +456,19 @@ omit =
homeassistant/components/zha/sensor.py homeassistant/components/zha/sensor.py
homeassistant/components/zha/switch.py homeassistant/components/zha/switch.py
homeassistant/components/zha/api.py homeassistant/components/zha/api.py
homeassistant/components/zha/entities/* homeassistant/components/zha/entity.py
homeassistant/components/zha/helpers.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/*/zha.py
homeassistant/components/zigbee.py homeassistant/components/zigbee.py
homeassistant/components/*/zigbee.py homeassistant/components/*/zigbee.py
homeassistant/components/zoneminder/* homeassistant/components/zoneminder/*
homeassistant/components/*/zoneminder.py
homeassistant/components/tuya.py homeassistant/components/tuya.py
homeassistant/components/*/tuya.py homeassistant/components/*/tuya.py
@ -460,6 +477,7 @@ omit =
homeassistant/components/*/spider.py homeassistant/components/*/spider.py
homeassistant/components/air_quality/opensensemap.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/alarmdotcom.py
homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
@ -552,6 +570,7 @@ omit =
homeassistant/components/device_tracker/sky_hub.py homeassistant/components/device_tracker/sky_hub.py
homeassistant/components/device_tracker/snmp.py homeassistant/components/device_tracker/snmp.py
homeassistant/components/device_tracker/swisscom.py homeassistant/components/device_tracker/swisscom.py
homeassistant/components/device_tracker/synology_srm.py
homeassistant/components/device_tracker/tado.py homeassistant/components/device_tracker/tado.py
homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tile.py homeassistant/components/device_tracker/tile.py
@ -574,6 +593,7 @@ omit =
homeassistant/components/image_processing/dlib_face_identify.py homeassistant/components/image_processing/dlib_face_identify.py
homeassistant/components/image_processing/seven_segments.py homeassistant/components/image_processing/seven_segments.py
homeassistant/components/image_processing/tensorflow.py homeassistant/components/image_processing/tensorflow.py
homeassistant/components/image_processing/qrcode.py
homeassistant/components/keyboard_remote.py homeassistant/components/keyboard_remote.py
homeassistant/components/keyboard.py homeassistant/components/keyboard.py
homeassistant/components/light/avion.py homeassistant/components/light/avion.py
@ -581,6 +601,7 @@ omit =
homeassistant/components/light/blinkt.py homeassistant/components/light/blinkt.py
homeassistant/components/light/decora_wifi.py homeassistant/components/light/decora_wifi.py
homeassistant/components/light/decora.py homeassistant/components/light/decora.py
homeassistant/components/light/everlights.py
homeassistant/components/light/flux_led.py homeassistant/components/light/flux_led.py
homeassistant/components/light/futurenow.py homeassistant/components/light/futurenow.py
homeassistant/components/light/greenwave.py homeassistant/components/light/greenwave.py
@ -719,7 +740,6 @@ omit =
homeassistant/components/sensor/aftership.py homeassistant/components/sensor/aftership.py
homeassistant/components/sensor/airvisual.py homeassistant/components/sensor/airvisual.py
homeassistant/components/sensor/alpha_vantage.py homeassistant/components/sensor/alpha_vantage.py
homeassistant/components/sensor/ambient_station.py
homeassistant/components/sensor/arest.py homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py homeassistant/components/sensor/arwn.py
homeassistant/components/sensor/bbox.py homeassistant/components/sensor/bbox.py
@ -744,7 +764,6 @@ omit =
homeassistant/components/sensor/dht.py homeassistant/components/sensor/dht.py
homeassistant/components/sensor/discogs.py homeassistant/components/sensor/discogs.py
homeassistant/components/sensor/dnsip.py homeassistant/components/sensor/dnsip.py
homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/domain_expiry.py homeassistant/components/sensor/domain_expiry.py
homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/dublin_bus_transport.py
@ -781,6 +800,7 @@ omit =
homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py homeassistant/components/sensor/htu21d.py
homeassistant/components/sensor/upnp.py homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/iliad_italy.py
homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap.py
homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/influxdb.py
@ -835,7 +855,9 @@ omit =
homeassistant/components/sensor/qnap.py homeassistant/components/sensor/qnap.py
homeassistant/components/sensor/radarr.py homeassistant/components/sensor/radarr.py
homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/rainbird.py
homeassistant/components/sensor/recollect_waste.py
homeassistant/components/sensor/ripple.py homeassistant/components/sensor/ripple.py
homeassistant/components/sensor/rova.py
homeassistant/components/sensor/rtorrent.py homeassistant/components/sensor/rtorrent.py
homeassistant/components/sensor/ruter.py homeassistant/components/sensor/ruter.py
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
@ -874,7 +896,6 @@ omit =
homeassistant/components/sensor/time_date.py homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/torque.py homeassistant/components/sensor/torque.py
homeassistant/components/sensor/trafikverket_weatherstation.py homeassistant/components/sensor/trafikverket_weatherstation.py
homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/travisci.py homeassistant/components/sensor/travisci.py
homeassistant/components/sensor/twitch.py homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py homeassistant/components/sensor/uber.py
@ -919,7 +940,6 @@ omit =
homeassistant/components/switch/switchmate.py homeassistant/components/switch/switchmate.py
homeassistant/components/switch/telnet.py homeassistant/components/switch/telnet.py
homeassistant/components/switch/tplink.py homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/vesync.py homeassistant/components/switch/vesync.py
homeassistant/components/telegram_bot/* homeassistant/components/telegram_bot/*
homeassistant/components/thingspeak.py 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/xmpp.py @fabaff
homeassistant/components/notify/yessssms.py @flowolf homeassistant/components/notify/yessssms.py @flowolf
homeassistant/components/plant.py @ChristianKuehnel homeassistant/components/plant.py @ChristianKuehnel
homeassistant/components/remote/harmony.py @ehendrix23
homeassistant/components/scene/lifx_cloud.py @amelchio homeassistant/components/scene/lifx_cloud.py @amelchio
homeassistant/components/sensor/airvisual.py @bachya homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/alpha_vantage.py @fabaff homeassistant/components/sensor/alpha_vantage.py @fabaff
@ -152,6 +153,7 @@ homeassistant/components/weather/openweathermap.py @fabaff
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
# A # A
homeassistant/components/ambient_station/* @bachya
homeassistant/components/arduino.py @fabaff homeassistant/components/arduino.py @fabaff
homeassistant/components/*/arduino.py @fabaff homeassistant/components/*/arduino.py @fabaff
homeassistant/components/*/arest.py @fabaff homeassistant/components/*/arest.py @fabaff
@ -234,6 +236,7 @@ homeassistant/components/*/rfxtrx.py @danielhiversen
# S # S
homeassistant/components/simplisafe/* @bachya homeassistant/components/simplisafe/* @bachya
homeassistant/components/smartthings/* @andrewsayre
# T # T
homeassistant/components/tahoma.py @philklei homeassistant/components/tahoma.py @philklei
@ -268,8 +271,7 @@ homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
# Z # Z
homeassistant/components/zoneminder/ @rohankapoorcom homeassistant/components/zoneminder/* @rohankapoorcom
homeassistant/components/*/zoneminder.py @rohankapoorcom
# Other code # Other code
homeassistant/scripts/check_config.py @kellerza homeassistant/scripts/check_config.py @kellerza

View File

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

View File

@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
ATTR_AQI = 'air_quality_index' ATTR_AQI = 'air_quality_index'
ATTR_ATTRIBUTION = 'attribution' ATTR_ATTRIBUTION = 'attribution'
ATTR_C02 = 'carbon_dioxide' ATTR_CO2 = 'carbon_dioxide'
ATTR_CO = 'carbon_monoxide' ATTR_CO = 'carbon_monoxide'
ATTR_N2O = 'nitrogen_oxide' ATTR_N2O = 'nitrogen_oxide'
ATTR_NO = 'nitrogen_monoxide' ATTR_NO = 'nitrogen_monoxide'
@ -35,7 +35,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
PROP_TO_ATTR = { PROP_TO_ATTR = {
'air_quality_index': ATTR_AQI, 'air_quality_index': ATTR_AQI,
'attribution': ATTR_ATTRIBUTION, 'attribution': ATTR_ATTRIBUTION,
'carbon_dioxide': ATTR_C02, 'carbon_dioxide': ATTR_CO2,
'carbon_monoxide': ATTR_CO, 'carbon_monoxide': ATTR_CO,
'nitrogen_oxide': ATTR_N2O, 'nitrogen_oxide': ATTR_N2O,
'nitrogen_monoxide': ATTR_NO, '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, ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) 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 import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent 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 logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice
from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED) STATE_ALARM_DISARMED)
@ -31,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(alarm_devices) add_entities(alarm_devices)
class AbodeAlarm(AbodeDevice, AlarmControlPanel): class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel):
"""An alarm_control_panel implementation for Abode.""" """An alarm_control_panel implementation for Abode."""
def __init__(self, data, device, name): def __init__(self, data, device, name):
@ -57,6 +57,11 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanel):
state = None state = None
return state return state
@property
def code_format(self):
"""Return one or more digits/characters."""
return alarm.FORMAT_NUMBER
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
self._device.set_standby() 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.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, 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 from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -57,7 +57,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
self._username = username self._username = username
self._password = password self._password = password
self._websession = async_get_clientsession(self._hass) self._websession = async_get_clientsession(self._hass)
self._state = STATE_UNKNOWN self._state = None
self._alarm = Alarmdotcom( self._alarm = Alarmdotcom(
username, password, self._websession, hass.loop) username, password, self._websession, hass.loop)
@ -93,7 +93,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
return STATE_ALARM_ARMED_HOME return STATE_ALARM_ARMED_HOME
if self._alarm.state.lower() == 'armed away': if self._alarm.state.lower() == 'armed away':
return STATE_ALARM_ARMED_AWAY return STATE_ALARM_ARMED_AWAY
return STATE_UNKNOWN return None
@property @property
def device_state_attributes(self): 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/ https://home-assistant.io/components/alarm_control_panel.concord232/
""" """
import datetime import datetime
from datetime import timedelta
import logging import logging
import requests import requests
import voluptuous as vol import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm 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.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY, CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['concord232==0.15'] REQUIREMENTS = ['concord232==0.15']
@ -26,7 +25,7 @@ DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'CONCORD232' DEFAULT_NAME = 'CONCORD232'
DEFAULT_PORT = 5007 DEFAULT_PORT = 5007
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = datetime.timedelta(seconds=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, 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) url = 'http://{}:{}'.format(host, port)
try: try:
add_entities([Concord232Alarm(hass, url, name)]) add_entities([Concord232Alarm(url, name)], True)
except requests.exceptions.ConnectionError as ex: except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to Concord232: %s", str(ex)) _LOGGER.error("Unable to connect to Concord232: %s", str(ex))
return
class Concord232Alarm(alarm.AlarmControlPanel): class Concord232Alarm(alarm.AlarmControlPanel):
"""Representation of the Concord232-based alarm panel.""" """Representation of the Concord232-based alarm panel."""
def __init__(self, hass, url, name): def __init__(self, url, name):
"""Initialize the Concord232 alarm panel.""" """Initialize the Concord232 alarm panel."""
from concord232 import client as concord232_client from concord232 import client as concord232_client
self._state = STATE_UNKNOWN self._state = None
self._hass = hass
self._name = name self._name = name
self._url = url self._url = url
self._alarm = concord232_client.Client(self._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.partitions = self._alarm.list_partitions() self._alarm.partitions = self._alarm.list_partitions()
self._alarm.last_partition_update = datetime.datetime.now() self._alarm.last_partition_update = datetime.datetime.now()
self.update()
@property @property
def name(self): def name(self):
@ -94,22 +84,17 @@ class Concord232Alarm(alarm.AlarmControlPanel):
except requests.exceptions.ConnectionError as ex: except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to %(host)s: %(reason)s", _LOGGER.error("Unable to connect to %(host)s: %(reason)s",
dict(host=self._url, reason=ex)) dict(host=self._url, reason=ex))
newstate = STATE_UNKNOWN return
except IndexError: except IndexError:
_LOGGER.error("Concord232 reports no partitions") _LOGGER.error("Concord232 reports no partitions")
newstate = STATE_UNKNOWN return
if part['arming_level'] == 'Off': if part['arming_level'] == 'Off':
newstate = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
elif 'Home' in part['arming_level']: elif 'Home' in part['arming_level']:
newstate = STATE_ALARM_ARMED_HOME self._state = STATE_ALARM_ARMED_HOME
else: else:
newstate = STATE_ALARM_ARMED_AWAY self._state = 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
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -76,7 +76,7 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
async def async_alarm_arm_home(self, code=None): async def async_alarm_arm_home(self, code=None):
"""Send arm home command.""" """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): async def async_alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""

View File

@ -14,7 +14,7 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, 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) STATE_ALARM_ARMED_CUSTOM_BYPASS)
@ -52,7 +52,7 @@ class TotalConnect(alarm.AlarmControlPanel):
self._name = name self._name = name
self._username = username self._username = username
self._password = password self._password = password
self._state = STATE_UNKNOWN self._state = None
self._client = TotalConnectClient.TotalConnectClient( self._client = TotalConnectClient.TotalConnectClient(
username, password) username, password)
@ -85,7 +85,7 @@ class TotalConnect(alarm.AlarmControlPanel):
elif status == self._client.DISARMING: elif status == self._client.DISARMING:
state = STATE_ALARM_DISARMING state = STATE_ALARM_DISARMING
else: else:
state = STATE_UNKNOWN state = None
self._state = state 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 CONF_ALARM, CONF_CODE_DIGITS
from homeassistant.components.verisure import HUB as hub from homeassistant.components.verisure import HUB as hub
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
STATE_UNKNOWN)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -44,7 +43,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
def __init__(self): def __init__(self):
"""Initialize the Verisure alarm panel.""" """Initialize the Verisure alarm panel."""
self._state = STATE_UNKNOWN self._state = None
self._digits = hub.config.get(CONF_CODE_DIGITS) self._digits = hub.config.get(CONF_CODE_DIGITS)
self._changed_by = None self._changed_by = None

View File

@ -9,8 +9,7 @@ import logging
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.components.wink import DOMAIN, WinkDevice
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
STATE_UNKNOWN)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -52,7 +51,7 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
elif wink_state == "night": elif wink_state == "night":
state = STATE_ALARM_ARMED_HOME state = STATE_ALARM_ARMED_HOME
else: else:
state = STATE_UNKNOWN state = None
return state return state
def alarm_disarm(self, code=None): 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/ https://home-assistant.io/components/alert/
""" """
import asyncio import asyncio
from datetime import datetime, timedelta
import logging import logging
from datetime import datetime, timedelta
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import ( 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 ( from homeassistant.const import (
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF, CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers import service, event from homeassistant.helpers import service, event
import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -30,6 +30,8 @@ CONF_REPEAT = 'repeat'
CONF_SKIP_FIRST = 'skip_first' CONF_SKIP_FIRST = 'skip_first'
CONF_ALERT_MESSAGE = 'message' CONF_ALERT_MESSAGE = 'message'
CONF_DONE_MESSAGE = 'done_message' CONF_DONE_MESSAGE = 'done_message'
CONF_TITLE = 'title'
CONF_DATA = 'data'
DEFAULT_CAN_ACK = True DEFAULT_CAN_ACK = True
DEFAULT_SKIP_FIRST = False DEFAULT_SKIP_FIRST = False
@ -43,13 +45,14 @@ ALERT_SCHEMA = vol.Schema({
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean, vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
vol.Optional(CONF_ALERT_MESSAGE): cv.template, vol.Optional(CONF_ALERT_MESSAGE): cv.template,
vol.Optional(CONF_DONE_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}) vol.Required(CONF_NOTIFIERS): cv.ensure_list})
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA), DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
ALERT_SERVICE_SCHEMA = vol.Schema({ ALERT_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids, 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) done_message_template = cfg.get(CONF_DONE_MESSAGE)
notifiers = cfg.get(CONF_NOTIFIERS) notifiers = cfg.get(CONF_NOTIFIERS)
can_ack = cfg.get(CONF_CAN_ACK) 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, entities.append(Alert(hass, object_id, name,
watched_entity_id, alert_state, repeat, watched_entity_id, alert_state, repeat,
skip_first, message_template, skip_first, message_template,
done_message_template, notifiers, done_message_template, notifiers,
can_ack)) can_ack, title_template, data))
if not entities: if not entities:
return False return False
@ -127,12 +132,14 @@ class Alert(ToggleEntity):
def __init__(self, hass, entity_id, name, watched_entity_id, def __init__(self, hass, entity_id, name, watched_entity_id,
state, repeat, skip_first, message_template, state, repeat, skip_first, message_template,
done_message_template, notifiers, can_ack): done_message_template, notifiers, can_ack, title_template,
data):
"""Initialize the alert.""" """Initialize the alert."""
self.hass = hass self.hass = hass
self._name = name self._name = name
self._alert_state = state self._alert_state = state
self._skip_first = skip_first self._skip_first = skip_first
self._data = data
self._message_template = message_template self._message_template = message_template
if self._message_template is not None: if self._message_template is not None:
@ -142,6 +149,10 @@ class Alert(ToggleEntity):
if self._done_message_template is not None: if self._done_message_template is not None:
self._done_message_template.hass = hass 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._notifiers = notifiers
self._can_ack = can_ack self._can_ack = can_ack
@ -251,9 +262,20 @@ class Alert(ToggleEntity):
await self._send_notification_message(message) await self._send_notification_message(message)
async def _send_notification_message(self, 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: for target in self._notifiers:
await self.hass.services.async_call( 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 def async_turn_on(self, **kwargs):
"""Async Unacknowledge alert.""" """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' 'whitebalance_lock': 'mdi:white-balance-auto'
} }
SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision', SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active',
'overlay', 'torch', 'whitebalance_lock', 'video_recording'] 'motion_detect', 'night_vision', 'overlay',
'torch', 'whitebalance_lock', 'video_recording']
SENSORS = ['audio_connections', 'battery_level', 'battery_temp', SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
'battery_voltage', 'light', 'motion', 'pressure', 'proximity', '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 import config_validation as cv
from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.discovery import async_load_platform
REQUIREMENTS = ['aioasuswrt==1.1.18'] REQUIREMENTS = ['aioasuswrt==1.1.20']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -9,7 +9,7 @@
}, },
"step": { "step": {
"init": { "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" "title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144"
}, },
"setup": { "setup": {

View File

@ -21,7 +21,7 @@
}, },
"totp": { "totp": {
"error": { "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": { "step": {
"init": { "init": {

View File

@ -3,6 +3,11 @@
"notify": { "notify": {
"error": { "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." "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'] DEPENDENCIES = ['mqtt']
CONF_ENCODING = 'encoding'
CONF_TOPIC = 'topic' CONF_TOPIC = 'topic'
DEFAULT_ENCODING = 'utf-8'
TRIGGER_SCHEMA = vol.Schema({ TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): mqtt.DOMAIN, vol.Required(CONF_PLATFORM): mqtt.DOMAIN,
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_PAYLOAD): cv.string, vol.Optional(CONF_PAYLOAD): cv.string,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
}) })
async def async_trigger(hass, config, action, automation_info): async def async_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
topic = config.get(CONF_TOPIC) topic = config[CONF_TOPIC]
payload = config.get(CONF_PAYLOAD) payload = config.get(CONF_PAYLOAD)
encoding = config[CONF_ENCODING] or None
@callback @callback
def mqtt_automation_listener(msg_topic, msg_payload, qos): 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( remove = await mqtt.async_subscribe(
hass, topic, mqtt_automation_listener) hass, topic, mqtt_automation_listener, encoding=encoding)
return remove return remove

View File

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

View File

@ -8,7 +8,6 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.maxcube import DATA_KEY from homeassistant.components.maxcube import DATA_KEY
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -40,7 +39,7 @@ class MaxCubeShutter(BinarySensorDevice):
self._sensor_type = 'window' self._sensor_type = 'window'
self._rf_address = rf_address self._rf_address = rf_address
self._cubehandle = handler self._cubehandle = handler
self._state = STATE_UNKNOWN self._state = None
@property @property
def should_poll(self): 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.helpers.event import async_track_state_change
from homeassistant.util import utcnow from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.15.4'] REQUIREMENTS = ['numpy==1.16.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -213,7 +213,8 @@ class ONVIFHassCamera(Camera):
if not self._input: if not self._input:
return None 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) loop=self.hass.loop)
await stream.open_camera( await stream.open_camera(
self._input, extra_cmd=self._ffmpeg_arguments) self._input, extra_cmd=self._ffmpeg_arguments)
@ -221,7 +222,7 @@ class ONVIFHassCamera(Camera):
try: try:
return await async_aiohttp_proxy_stream( return await async_aiohttp_proxy_stream(
self.hass, request, stream, self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver') ffmpeg_manager.ffmpeg_stream_content_type)
finally: finally:
await stream.close() await stream.close()

View File

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

View File

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

View File

@ -147,6 +147,6 @@ class YiCamera(Camera):
try: try:
return await async_aiohttp_proxy_stream( return await async_aiohttp_proxy_stream(
self.hass, request, stream, self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver') self._manager.ffmpeg_stream_content_type)
finally: finally:
await stream.close() 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 import homeassistant.helpers.config_validation as cv
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, 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) PRECISION_TENTHS)
DEFAULT_MIN_TEMP = 7 DEFAULT_MIN_TEMP = 7
@ -208,7 +208,7 @@ class ClimateDevice(Entity):
return self.current_operation return self.current_operation
if self.is_on: if self.is_on:
return STATE_ON return STATE_ON
return STATE_UNKNOWN return None
@property @property
def precision(self): def precision(self):

View File

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

View File

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

View File

@ -219,6 +219,11 @@ class RadioThermostat(ClimateDevice):
"""Return true if away mode is on.""" """Return true if away mode is on."""
return self._away return self._away
@property
def is_on(self):
"""Return true if on."""
return self._tstate != STATE_IDLE
def update(self): def update(self):
"""Update and validate the data from the thermostat.""" """Update and validate the data from the thermostat."""
# Radio thermostats are very slow, and sometimes don't respond # 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) 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) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start)
await http_api.async_setup(hass) await http_api.async_setup(hass)
return True return True
@ -263,7 +264,7 @@ class Cloud:
self.access_token = info['access_token'] self.access_token = info['access_token']
self.refresh_token = info['refresh_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 def _decode_claims(self, token): # pylint: disable=no-self-use
"""Decode the claims in a token.""" """Decode the claims in a token."""

View File

@ -1,4 +1,10 @@
"""Package to communicate with the authentication API.""" """Package to communicate with the authentication API."""
import asyncio
import logging
import random
_LOGGER = logging.getLogger(__name__)
class CloudError(Exception): 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): def _map_aws_exception(err):
"""Map AWS exception to our exceptions.""" """Map AWS exception to our exceptions."""
ex = AWS_EXCEPTIONS.get(err.response['Error']['Code'], UnknownError) ex = AWS_EXCEPTIONS.get(err.response['Error']['Code'], UnknownError)
@ -47,7 +87,7 @@ def _map_aws_exception(err):
def register(cloud, email, password): def register(cloud, email, password):
"""Register a new account.""" """Register a new account."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(cloud) cognito = _cognito(cloud)
# Workaround for bug in Warrant. PR with fix: # Workaround for bug in Warrant. PR with fix:
@ -55,13 +95,16 @@ def register(cloud, email, password):
cognito.add_base_attributes() cognito.add_base_attributes()
try: try:
cognito.register(email, password) cognito.register(email, password)
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def resend_email_confirm(cloud, email): def resend_email_confirm(cloud, email):
"""Resend email confirmation.""" """Resend email confirmation."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(cloud, username=email) cognito = _cognito(cloud, username=email)
@ -72,18 +115,23 @@ def resend_email_confirm(cloud, email):
) )
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def forgot_password(cloud, email): def forgot_password(cloud, email):
"""Initialize forgotten password flow.""" """Initialize forgotten password flow."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito(cloud, username=email) cognito = _cognito(cloud, username=email)
try: try:
cognito.initiate_forgot_password() cognito.initiate_forgot_password()
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def login(cloud, email, password): def login(cloud, email, password):
@ -97,7 +145,7 @@ def login(cloud, email, password):
def check_token(cloud): def check_token(cloud):
"""Check that the token is valid and verify if needed.""" """Check that the token is valid and verify if needed."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito( cognito = _cognito(
cloud, cloud,
@ -109,13 +157,17 @@ def check_token(cloud):
cloud.id_token = cognito.id_token cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token cloud.access_token = cognito.access_token
cloud.write_user_info() cloud.write_user_info()
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def renew_access_token(cloud): def renew_access_token(cloud):
"""Renew access token.""" """Renew access token."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError, EndpointConnectionError
cognito = _cognito( cognito = _cognito(
cloud, cloud,
@ -127,13 +179,17 @@ def renew_access_token(cloud):
cloud.id_token = cognito.id_token cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token cloud.access_token = cognito.access_token
cloud.write_user_info() cloud.write_user_info()
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def _authenticate(cloud, email, password): def _authenticate(cloud, email, password):
"""Log in and return an authenticated Cognito instance.""" """Log in and return an authenticated Cognito instance."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError, EndpointConnectionError
from warrant.exceptions import ForceChangePasswordException from warrant.exceptions import ForceChangePasswordException
assert not cloud.is_logged_in, 'Cannot login if already logged in.' assert not cloud.is_logged_in, 'Cannot login if already logged in.'
@ -145,11 +201,14 @@ def _authenticate(cloud, email, password):
return cognito return cognito
except ForceChangePasswordException: except ForceChangePasswordException:
raise PasswordChangeRequired raise PasswordChangeRequired()
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
except EndpointConnectionError:
raise UnknownError()
def _cognito(cloud, **kwargs): def _cognito(cloud, **kwargs):
"""Get the client credentials.""" """Get the client credentials."""

View File

@ -107,13 +107,16 @@ def _handle_cloud_errors(handler):
result = await handler(view, request, *args, **kwargs) result = await handler(view, request, *args, **kwargs)
return result 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__) err_info = _CLOUD_ERRORS.get(err.__class__)
if err_info is None: if err_info is None:
_LOGGER.exception(
"Unexpected error processing request for %s", request.path)
err_info = (502, 'Unexpected error: {}'.format(err)) err_info = (502, 'Unexpected error: {}'.format(err))
status, msg = err_info status, msg = err_info
return view.json_message(msg, status_code=status, return view.json_message(
message_code=err.__class__.__name__) msg, status_code=status,
message_code=err.__class__.__name__.lower())
return error_handler 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.components.google_assistant import smart_home as ga
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.util.decorator import Registry 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 homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import auth_api from . import auth_api
from . import utils
from .const import MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL from .const import MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL
HANDLERS = Registry() HANDLERS = Registry()
@ -61,12 +62,18 @@ class CloudIoT:
# Local code waiting for a response # Local code waiting for a response
self._response_handler = {} self._response_handler = {}
self._on_connect = [] self._on_connect = []
self._on_disconnect = []
@callback @callback
def register_on_connect(self, on_connect_cb): def register_on_connect(self, on_connect_cb):
"""Register an async on_connect callback.""" """Register an async on_connect callback."""
self._on_connect.append(on_connect_cb) 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 @property
def connected(self): def connected(self):
"""Return if we're currently connected.""" """Return if we're currently connected."""
@ -101,6 +108,17 @@ class CloudIoT:
# Still adding it here to make sure we can always reconnect # Still adding it here to make sure we can always reconnect
_LOGGER.exception("Unexpected error") _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: if self.close_requested:
break break
@ -191,7 +209,13 @@ class CloudIoT:
self.state = STATE_CONNECTED self.state = STATE_CONNECTED
if self._on_connect: if self._on_connect:
try:
yield from asyncio.wait([cb() for cb in self._on_connect]) 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: while not client.closed:
msg = yield from client.receive() msg = yield from client.receive()
@ -325,11 +349,6 @@ async def async_handle_cloud(hass, cloud, payload):
await cloud.logout() await cloud.logout()
_LOGGER.error("You have been logged out from Home Assistant cloud: %s", _LOGGER.error("You have been logged out from Home Assistant cloud: %s",
payload['reason']) 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: else:
_LOGGER.warning("Received unknown cloud action: %s", action) _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( response = await hass.components.webhook.async_handle_webhook(
found['webhook_id'], request) found['webhook_id'], request)
response_dict = serialize_response(response) response_dict = utils.aiohttp_serialize_response(response)
body = response_dict.get('body') body = response_dict.get('body')
if body:
body = body.decode('utf-8')
return { return {
'body': body, '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' DOMAIN = 'config'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
SECTIONS = ( SECTIONS = (
'area_registry',
'auth', 'auth',
'auth_provider_homeassistant', 'auth_provider_homeassistant',
'automation', '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.""" """HTTP views to interact with the device registry."""
import voluptuous as vol import voluptuous as vol
from homeassistant.helpers.device_registry import async_get_registry
from homeassistant.components import websocket_api 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'] DEPENDENCIES = ['websocket_api']
@ -11,22 +14,53 @@ SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_LIST, 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): async def async_setup(hass):
"""Enable the Entity Registry views.""" """Enable the Device Registry views."""
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
WS_TYPE_LIST, websocket_list_devices, WS_TYPE_LIST, websocket_list_devices,
SCHEMA_WS_LIST SCHEMA_WS_LIST
) )
hass.components.websocket_api.async_register_command(
WS_TYPE_UPDATE, websocket_update_device, SCHEMA_WS_UPDATE
)
return True return True
@websocket_api.async_response @async_response
async def websocket_list_devices(hass, connection, msg): async def websocket_list_devices(hass, connection, msg):
"""Handle list devices command.""" """Handle list devices command."""
registry = await async_get_registry(hass) registry = await async_get_registry(hass)
connection.send_message(websocket_api.result_message( connection.send_message(websocket_api.result_message(
msg['id'], [{ 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), 'config_entries': list(entry.config_entries),
'connections': list(entry.connections), 'connections': list(entry.connections),
'manufacturer': entry.manufacturer, 'manufacturer': entry.manufacturer,
@ -35,5 +69,5 @@ async def websocket_list_devices(hass, connection, msg):
'sw_version': entry.sw_version, 'sw_version': entry.sw_version,
'id': entry.id, 'id': entry.id,
'hub_device_id': entry.hub_device_id, 'hub_device_id': entry.hub_device_id,
} for entry in registry.devices.values()] '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.helpers.entity_registry import async_get_registry
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.websocket_api.const import ERR_NOT_FOUND 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 from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['websocket_api'] DEPENDENCIES = ['websocket_api']
@ -30,6 +31,12 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Optional('new_entity_id'): str, 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): async def async_setup(hass):
"""Enable the Entity Registry views.""" """Enable the Entity Registry views."""
@ -45,6 +52,10 @@ async def async_setup(hass):
WS_TYPE_UPDATE, websocket_update_entity, WS_TYPE_UPDATE, websocket_update_entity,
SCHEMA_WS_UPDATE SCHEMA_WS_UPDATE
) )
hass.components.websocket_api.async_register_command(
WS_TYPE_REMOVE, websocket_remove_entity,
SCHEMA_WS_REMOVE
)
return True return True
@ -56,14 +67,7 @@ async def websocket_list_entities(hass, connection, msg):
""" """
registry = await async_get_registry(hass) registry = await async_get_registry(hass)
connection.send_message(websocket_api.result_message( connection.send_message(websocket_api.result_message(
msg['id'], [{ msg['id'], [_entry_dict(entry) for entry in registry.entities.values()]
'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()]
)) ))
@ -86,6 +90,7 @@ async def websocket_get_entity(hass, connection, msg):
)) ))
@require_admin
@async_response @async_response
async def websocket_update_entity(hass, connection, msg): async def websocket_update_entity(hass, connection, msg):
"""Handle update entity websocket command. """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 @callback
def _entry_dict(entry): def _entry_dict(entry):
"""Convert entry to API format.""" """Convert entry to API format."""
return { return {
'config_entry_id': entry.config_entry_id,
'device_id': entry.device_id,
'disabled_by': entry.disabled_by,
'entity_id': entry.entity_id, '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_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT, SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION, STATE_OPEN, 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__) _LOGGER = logging.getLogger(__name__)
@ -178,7 +178,7 @@ class CoverDevice(Entity):
closed = self.is_closed closed = self.is_closed
if closed is None: if closed is None:
return STATE_UNKNOWN return None
return STATE_CLOSED if closed else STATE_OPEN 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.helpers.event import track_utc_time_change
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE, CONF_USERNAME, CONF_PASSWORD, CONF_ACCESS_TOKEN, CONF_NAME, 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__) _LOGGER = logging.getLogger(__name__)
@ -83,7 +83,7 @@ class GaradgetCover(CoverDevice):
self.obtained_token = False self.obtained_token = False
self._username = args['username'] self._username = args['username']
self._password = args['password'] self._password = args['password']
self._state = STATE_UNKNOWN self._state = None
self.time_in_state = None self.time_in_state = None
self.signal = None self.signal = None
self.sensor = None self.sensor = None
@ -156,7 +156,7 @@ class GaradgetCover(CoverDevice):
@property @property
def is_closed(self): def is_closed(self):
"""Return if the cover is closed.""" """Return if the cover is closed."""
if self._state == STATE_UNKNOWN: if self._state is None:
return None return None
return self._state == STATE_CLOSED return self._state == STATE_CLOSED
@ -226,7 +226,7 @@ class GaradgetCover(CoverDevice):
try: try:
status = self._get_variable('doorStatus') status = self._get_variable('doorStatus')
_LOGGER.debug("Current Status: %s", status['status']) _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.time_in_state = status['time']
self.signal = status['signal'] self.signal = status['signal']
self.sensor = status['sensor'] 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) STATE_OPEN, STATE_OPENING)
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
REQUIREMENTS = ['pymyq==1.0.0'] REQUIREMENTS = ['pymyq==1.1.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MYQ_TO_HASS = { 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.wink/ https://home-assistant.io/components/cover.wink/
""" """
from homeassistant.components.cover import CoverDevice, STATE_UNKNOWN, \ from homeassistant.components.cover import CoverDevice, ATTR_POSITION
ATTR_POSITION
from homeassistant.components.wink import WinkDevice, DOMAIN from homeassistant.components.wink import WinkDevice, DOMAIN
DEPENDENCIES = ['wink'] DEPENDENCIES = ['wink']
@ -54,7 +53,7 @@ class WinkCoverDevice(WinkDevice, CoverDevice):
"""Return the current position of cover shutter.""" """Return the current position of cover shutter."""
if self.wink.state() is not None: if self.wink.state() is not None:
return int(self.wink.state()*100) return int(self.wink.state()*100)
return STATE_UNKNOWN return None
@property @property
def is_closed(self): 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" "title": "Define deCONZ gateway"
}, },
"link": { "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" "title": "Link with deCONZ"
}, },
"options": { "options": {

View File

@ -12,7 +12,7 @@
"init": { "init": {
"data": { "data": {
"host": "Host", "host": "Host",
"port": "Port (warto\u015b\u0107 domy\u015blna: \"80\")" "port": "Port"
}, },
"title": "Zdefiniuj bramk\u0119 deCONZ" "title": "Zdefiniuj bramk\u0119 deCONZ"
}, },
@ -28,6 +28,6 @@
"title": "Dodatkowe opcje konfiguracji dla deCONZ" "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": { "config": {
"abort": { "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." "one_instance_allowed": "Potrebna je samo ena instanca."
}, },
"create_entry": { "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": { "step": {
"user": { "user": {

View File

@ -1,10 +1,15 @@
{ {
"config": { "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": { "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" "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": { "step": {
"user": { "user": {
"description": "\u60a8\u786e\u5b9a\u8981\u8bbe\u7f6e Dialogflow \u5417?",
"title": "\u8bbe\u7f6e Dialogflow Webhook" "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 ( from .hue_api import (
HueUsernameView, HueAllLightsStateView, HueOneLightStateView, HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
HueOneLightChangeView, HueGroupView) HueOneLightChangeView, HueGroupView, HueAllGroupsStateView)
from .upnp import DescriptionXmlView, UPNPResponderThread from .upnp import DescriptionXmlView, UPNPResponderThread
DOMAIN = 'emulated_hue' DOMAIN = 'emulated_hue'
@ -105,6 +105,7 @@ async def async_setup(hass, yaml_config):
HueAllLightsStateView(config).register(app, app.router) HueAllLightsStateView(config).register(app, app.router)
HueOneLightStateView(config).register(app, app.router) HueOneLightStateView(config).register(app, app.router)
HueOneLightChangeView(config).register(app, app.router) HueOneLightChangeView(config).register(app, app.router)
HueAllGroupsStateView(config).register(app, app.router)
HueGroupView(config).register(app, app.router) HueGroupView(config).register(app, app.router)
upnp_listener = UPNPResponderThread( upnp_listener = UPNPResponderThread(

View File

@ -56,6 +56,28 @@ class HueUsernameView(HomeAssistantView):
return self.json([{'success': {'username': '12345678901234567890'}}]) 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): class HueGroupView(HomeAssistantView):
"""Group handler to get Logitech Pop working.""" """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", "host_ip": "\ud638\uc2a4\ud2b8 IP",
"listen_port": "\uc218\uc2e0 \ud3ec\ud2b8", "listen_port": "\uc218\uc2e0 \ud3ec\ud2b8",
"name": "\uc774\ub984", "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" "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_ADVERTISE_IP, CONF_ADVERTISE_PORT, CONF_HOST_IP, CONF_LISTEN_PORT,
CONF_SERVERS, CONF_UPNP_BIND_MULTICAST, DOMAIN) CONF_SERVERS, CONF_UPNP_BIND_MULTICAST, DOMAIN)
REQUIREMENTS = ['emulated_roku==0.1.7'] REQUIREMENTS = ['emulated_roku==0.1.8']
SERVER_CONFIG_SCHEMA = vol.Schema({ SERVER_CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string, vol.Required(CONF_NAME): cv.string,

View File

@ -146,19 +146,19 @@ async def async_setup(hass, config):
@callback @callback
def zones_updated_callback(data): def zones_updated_callback(data):
"""Handle zone timer updates.""" """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) async_dispatcher_send(hass, SIGNAL_ZONE_UPDATE, data)
@callback @callback
def alarm_data_updated_callback(data): def alarm_data_updated_callback(data):
"""Handle non-alarm based info updates.""" """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) async_dispatcher_send(hass, SIGNAL_KEYPAD_UPDATE, data)
@callback @callback
def partition_updated_callback(data): def partition_updated_callback(data):
"""Handle partition changes thrown by evl (including alarms).""" """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) async_dispatcher_send(hass, SIGNAL_PARTITION_UPDATE, data)
@callback @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

@ -1,10 +1,10 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "ESP jest ju\u017c skonfigurowany" "already_configured": "ESP jest ju\u017c skonfigurowane"
}, },
"error": { "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!", "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" "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"
}, },

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.components import group
from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE, from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TOGGLE,
SERVICE_TURN_OFF, ATTR_ENTITY_ID, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
STATE_UNKNOWN)
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent 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.""" """Return if the fans are on based on the statemachine."""
entity_id = entity_id or ENTITY_ID_ALL_FANS entity_id = entity_id or ENTITY_ID_ALL_FANS
state = hass.states.get(entity_id) 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): async def async_setup(hass, config: dict):
@ -199,7 +198,7 @@ class FanEntity(ToggleEntity):
@property @property
def is_on(self): def is_on(self):
"""Return true if the entity is on.""" """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 @property
def speed(self) -> str: def speed(self) -> str:

View File

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

View File

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

View File

@ -38,7 +38,7 @@ MODEL_AIRPURIFIER_MA1 = 'zhimi.airpurifier.ma1'
MODEL_AIRPURIFIER_MA2 = 'zhimi.airpurifier.ma2' MODEL_AIRPURIFIER_MA2 = 'zhimi.airpurifier.ma2'
MODEL_AIRPURIFIER_SA1 = 'zhimi.airpurifier.sa1' MODEL_AIRPURIFIER_SA1 = 'zhimi.airpurifier.sa1'
MODEL_AIRPURIFIER_SA2 = 'zhimi.airpurifier.sa2' 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_V1 = 'zhimi.humidifier.v1'
MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1' MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1'
@ -62,7 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
MODEL_AIRPURIFIER_MA2, MODEL_AIRPURIFIER_MA2,
MODEL_AIRPURIFIER_SA1, MODEL_AIRPURIFIER_SA1,
MODEL_AIRPURIFIER_SA2, MODEL_AIRPURIFIER_SA2,
MODEL_AIRPURIFIER_MC1, MODEL_AIRPURIFIER_2S,
MODEL_AIRHUMIDIFIER_V1, MODEL_AIRHUMIDIFIER_V1,
MODEL_AIRHUMIDIFIER_CA, MODEL_AIRHUMIDIFIER_CA,
MODEL_AIRFRESH_VA2, MODEL_AIRFRESH_VA2,
@ -175,6 +175,15 @@ AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 = {
ATTR_VOLUME: 'volume', 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 = { AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = {
# Common set isn't used here. It's a very basic version of the device. # Common set isn't used here. It's a very basic version of the device.
ATTR_AIR_QUALITY_INDEX: 'aqi', ATTR_AIR_QUALITY_INDEX: 'aqi',
@ -249,6 +258,7 @@ AVAILABLE_ATTRIBUTES_AIRFRESH = {
OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle'] OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle']
OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite'] OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite']
OPERATION_MODES_AIRPURIFIER_PRO_V7 = OPERATION_MODES_AIRPURIFIER_PRO OPERATION_MODES_AIRPURIFIER_PRO_V7 = OPERATION_MODES_AIRPURIFIER_PRO
OPERATION_MODES_AIRPURIFIER_2S = ['Auto', 'Silent', 'Favorite']
OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle', OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle',
'Medium', 'High', 'Strong'] 'Medium', 'High', 'Strong']
OPERATION_MODES_AIRFRESH = ['Auto', 'Silent', 'Interval', 'Low', 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_FAVORITE_LEVEL |
FEATURE_SET_VOLUME) 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_FLAGS_AIRPURIFIER_V3 = (FEATURE_SET_BUZZER |
FEATURE_SET_CHILD_LOCK | FEATURE_SET_CHILD_LOCK |
FEATURE_SET_LED) FEATURE_SET_LED)
@ -619,6 +634,10 @@ class XiaomiAirPurifier(XiaomiGenericDevice):
self._available_attributes = \ self._available_attributes = \
AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7
self._speed_list = OPERATION_MODES_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: elif self._model == MODEL_AIRPURIFIER_V3:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3 self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3
self._available_attributes = AVAILABLE_ATTRIBUTES_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/ https://home-assistant.io/components/ffmpeg/
""" """
import logging import logging
import re
import voluptuous as vol import voluptuous as vol
@ -16,7 +17,7 @@ from homeassistant.helpers.dispatcher import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['ha-ffmpeg==1.9'] REQUIREMENTS = ['ha-ffmpeg==1.11']
DOMAIN = 'ffmpeg' DOMAIN = 'ffmpeg'
@ -60,6 +61,8 @@ async def async_setup(hass, config):
conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY) conf.get(CONF_FFMPEG_BIN, DEFAULT_BINARY)
) )
await manager.async_get_version()
# Register service # Register service
async def async_service_handle(service): async def async_service_handle(service):
"""Handle service ffmpeg process.""" """Handle service ffmpeg process."""
@ -96,12 +99,37 @@ class FFmpegManager:
self.hass = hass self.hass = hass
self._cache = {} self._cache = {}
self._bin = ffmpeg_bin self._bin = ffmpeg_bin
self._version = None
self._major_version = None
@property @property
def binary(self): def binary(self):
"""Return ffmpeg binary from config.""" """Return ffmpeg binary from config."""
return self._bin 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): class FFmpegBase(Entity):
"""Interface object for FFmpeg.""" """Interface object for FFmpeg."""

View File

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

View File

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

View File

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

View File

@ -22,15 +22,12 @@ DOMAIN = 'geo_location'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
GROUP_NAME_ALL_EVENTS = 'All Geolocation Events'
SCAN_INTERVAL = timedelta(seconds=60) SCAN_INTERVAL = timedelta(seconds=60)
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up the Geolocation component.""" """Set up the Geolocation component."""
component = EntityComponent( component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_EVENTS)
await component.async_setup(config) await component.async_setup(config)
return True return True

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