Merge pull request #13856 from home-assistant/rc

0.67.0
This commit is contained in:
Paulus Schoutsen 2018-04-13 17:59:39 -04:00 committed by GitHub
commit fb91b05051
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
189 changed files with 4949 additions and 2705 deletions

View File

@ -160,9 +160,6 @@ omit =
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
homeassistant/components/mercedesme.py
homeassistant/components/*/mercedesme.py
homeassistant/components/mochad.py
homeassistant/components/*/mochad.py
@ -289,11 +286,9 @@ omit =
homeassistant/components/*/wink.py
homeassistant/components/xiaomi_aqara.py
homeassistant/components/binary_sensor/xiaomi_aqara.py
homeassistant/components/cover/xiaomi_aqara.py
homeassistant/components/light/xiaomi_aqara.py
homeassistant/components/sensor/xiaomi_aqara.py
homeassistant/components/switch/xiaomi_aqara.py
homeassistant/components/*/xiaomi_aqara.py
homeassistant/components/*/xiaomi_miio.py
homeassistant/components/zabbix.py
homeassistant/components/*/zabbix.py
@ -357,6 +352,7 @@ omit =
homeassistant/components/climate/touchline.py
homeassistant/components/climate/venstar.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/gogogate2.py
homeassistant/components/cover/homematic.py
homeassistant/components/cover/knx.py
homeassistant/components/cover/myq.py
@ -374,6 +370,7 @@ omit =
homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/google_maps.py
homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/hitron_coda.py
homeassistant/components/device_tracker/huawei_router.py
@ -400,8 +397,8 @@ omit =
homeassistant/components/emoncms_history.py
homeassistant/components/emulated_hue/upnp.py
homeassistant/components/fan/mqtt.py
homeassistant/components/fan/xiaomi_miio.py
homeassistant/components/feedreader.py
homeassistant/components/folder_watcher.py
homeassistant/components/foursquare.py
homeassistant/components/goalfeed.py
homeassistant/components/ifttt.py
@ -424,6 +421,7 @@ omit =
homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py
homeassistant/components/light/mystrom.py
homeassistant/components/light/nanoleaf_aurora.py
homeassistant/components/light/osramlightify.py
homeassistant/components/light/piglow.py
homeassistant/components/light/rpi_gpio_pwm.py
@ -432,7 +430,6 @@ omit =
homeassistant/components/light/tplink.py
homeassistant/components/light/tradfri.py
homeassistant/components/light/x10.py
homeassistant/components/light/xiaomi_miio.py
homeassistant/components/light/yeelight.py
homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/zengge.py
@ -441,6 +438,7 @@ omit =
homeassistant/components/lock/nello.py
homeassistant/components/lock/nuki.py
homeassistant/components/lock/sesame.py
homeassistant/components/map.py
homeassistant/components/media_extractor.py
homeassistant/components/media_player/anthemav.py
homeassistant/components/media_player/aquostv.py
@ -508,6 +506,7 @@ omit =
homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py
homeassistant/components/notify/mastodon.py
homeassistant/components/notify/matrix.py
homeassistant/components/notify/message_bird.py
homeassistant/components/notify/mycroft.py
@ -523,8 +522,8 @@ omit =
homeassistant/components/notify/sendgrid.py
homeassistant/components/notify/simplepush.py
homeassistant/components/notify/slack.py
homeassistant/components/notify/stride.py
homeassistant/components/notify/smtp.py
homeassistant/components/notify/stride.py
homeassistant/components/notify/synology_chat.py
homeassistant/components/notify/syslog.py
homeassistant/components/notify/telegram.py
@ -538,7 +537,6 @@ omit =
homeassistant/components/remember_the_milk/__init__.py
homeassistant/components/remote/harmony.py
homeassistant/components/remote/itach.py
homeassistant/components/remote/xiaomi_miio.py
homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/scene/lifx_cloud.py
homeassistant/components/sensor/airvisual.py
@ -674,6 +672,7 @@ omit =
homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/viaggiatreno.py
homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/waze_travel_time.py
homeassistant/components/sensor/whois.py
homeassistant/components/sensor/worldtidesinfo.py
homeassistant/components/sensor/worxlandroid.py
@ -707,7 +706,6 @@ omit =
homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/vesync.py
homeassistant/components/switch/xiaomi_miio.py
homeassistant/components/telegram_bot/*
homeassistant/components/thingspeak.py
homeassistant/components/tts/amazon_polly.py
@ -716,7 +714,6 @@ omit =
homeassistant/components/tts/picotts.py
homeassistant/components/vacuum/mqtt.py
homeassistant/components/vacuum/roomba.py
homeassistant/components/vacuum/xiaomi_miio.py
homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.py

View File

@ -1,35 +1,45 @@
Make sure you are running the latest version of Home Assistant before reporting an issue.
<!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
- Do not report issues for components if you are using custom components: files in <config-dir>/custom_components
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
-->
You should only file an issue if you found a bug. Feature and enhancement requests should go in [the Feature Requests section](https://community.home-assistant.io/c/feature-requests) of our community forum:
**Home Assistant release (`hass --version`):**
**Home Assistant release with the issue:**
<!--
- Frontend -> Developer tools -> Info
- Or use this command: hass --version
-->
**Python release (`python3 --version`):**
**Last working Home Assistant release (if known):**
**Operating environment (Hass.io/Docker/Windows/etc.):**
<!--
Please provide details about your environment.
-->
**Component/platform:**
<!--
Please add the link to the documentation at https://www.home-assistant.io/components/ of the component/platform in question.
-->
**Description of problem:**
**Expected:**
**Problem-relevant `configuration.yaml` entries and steps to reproduce:**
**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
```yaml
```
1.
2.
3.
**Traceback (if applicable):**
```bash
```
```
**Additional info:**
**Additional information:**

View File

@ -29,9 +29,6 @@ homeassistant/components/weblink.py @home-assistant/core
homeassistant/components/websocket_api.py @home-assistant/core
homeassistant/components/zone.py @home-assistant/core
# To monitor non-pypi additions
requirements_all.txt @andrey-git
# HomeAssistant developer Teams
Dockerfile @home-assistant/docker
virtualization/Docker/* @home-assistant/docker
@ -43,6 +40,7 @@ homeassistant/components/hassio.py @home-assistant/hassio
# Individual components
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
homeassistant/components/binary_sensor/hikvision.py @mezz64
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/camera/yi.py @bachya
@ -69,8 +67,10 @@ homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/pollen.py @bachya
homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/qnap.py @colinodell
homeassistant/components/sensor/sma.py @kellerza
homeassistant/components/sensor/sql.py @dgomes
homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya
@ -80,17 +80,17 @@ homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/axis.py @kane610
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/*/deconz.py @kane610
homeassistant/components/eight_sleep.py @mezz64
homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p
homeassistant/components/*/deconz.py @kane610
homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342
homeassistant/components/knx.py @Julius2342
homeassistant/components/*/knx.py @Julius2342
homeassistant/components/qwikswitch.py @kellerza
homeassistant/components/*/qwikswitch.py @kellerza
homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/tahoma.py @philklei
homeassistant/components/*/tahoma.py @philklei
homeassistant/components/tesla.py @zabuldon
@ -98,5 +98,9 @@ homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tradfri.py @ggravlingen
homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
homeassistant/scripts/check_config.py @kellerza

View File

@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['abodepy==0.12.2']
REQUIREMENTS = ['abodepy==0.12.3']
_LOGGER = logging.getLogger(__name__)

View File

@ -93,7 +93,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class IFTTTAlarmPanel(alarm.AlarmControlPanel):
"""Representation of an alarm control panel controlled throught IFTTT."""
"""Representation of an alarm control panel controlled through IFTTT."""
def __init__(self, name, code, event_away, event_home, event_night,
event_disarm, optimistic):

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_CUSTOM_BYPASS)
REQUIREMENTS = ['total_connect_client==0.16']
REQUIREMENTS = ['total_connect_client==0.17']
_LOGGER = logging.getLogger(__name__)

View File

@ -6,18 +6,20 @@ from datetime import datetime
from uuid import uuid4
from homeassistant.components import (
alert, automation, cover, fan, group, input_boolean, light, lock,
alert, automation, cover, climate, fan, group, input_boolean, light, lock,
media_player, scene, script, switch, http, sensor)
import homeassistant.core as ha
import homeassistant.util.color as color_util
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.util.decorator import Registry
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME,
SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
_LOGGER = logging.getLogger(__name__)
@ -34,6 +36,16 @@ API_TEMP_UNITS = {
TEMP_CELSIUS: 'CELSIUS',
}
API_THERMOSTAT_MODES = {
climate.STATE_HEAT: 'HEAT',
climate.STATE_COOL: 'COOL',
climate.STATE_AUTO: 'AUTO',
climate.STATE_ECO: 'ECO',
climate.STATE_IDLE: 'OFF',
climate.STATE_FAN_ONLY: 'OFF',
climate.STATE_DRY: 'OFF',
}
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
CONF_DESCRIPTION = 'description'
@ -383,8 +395,60 @@ class _AlexaTemperatureSensor(_AlexaInterface):
raise _UnsupportedProperty(name)
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
temp = self.entity.state
if self.entity.domain == climate.DOMAIN:
temp = self.entity.attributes.get(
climate.ATTR_CURRENT_TEMPERATURE)
return {
'value': float(self.entity.state),
'value': float(temp),
'scale': API_TEMP_UNITS[unit],
}
class _AlexaThermostatController(_AlexaInterface):
def name(self):
return 'Alexa.ThermostatController'
def properties_supported(self):
properties = []
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & climate.SUPPORT_TARGET_TEMPERATURE:
properties.append({'name': 'targetSetpoint'})
if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW:
properties.append({'name': 'lowerSetpoint'})
if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH:
properties.append({'name': 'upperSetpoint'})
if supported & climate.SUPPORT_OPERATION_MODE:
properties.append({'name': 'thermostatMode'})
return properties
def properties_retrievable(self):
return True
def get_property(self, name):
if name == 'thermostatMode':
ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE)
mode = API_THERMOSTAT_MODES.get(ha_mode)
if mode is None:
_LOGGER.error("%s (%s) has unsupported %s value '%s'",
self.entity.entity_id, type(self.entity),
climate.ATTR_OPERATION_MODE, ha_mode)
raise _UnsupportedProperty(name)
return mode
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
temp = None
if name == 'targetSetpoint':
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
elif name == 'lowerSetpoint':
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
elif name == 'upperSetpoint':
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
if temp is None:
raise _UnsupportedProperty(name)
return {
'value': float(temp),
'scale': API_TEMP_UNITS[unit],
}
@ -415,6 +479,16 @@ class _SwitchCapabilities(_AlexaEntity):
return [_AlexaPowerController(self.entity)]
@ENTITY_ADAPTERS.register(climate.DOMAIN)
class _ClimateCapabilities(_AlexaEntity):
def default_display_categories(self):
return [_DisplayCategory.THERMOSTAT]
def interfaces(self):
yield _AlexaThermostatController(self.entity)
yield _AlexaTemperatureSensor(self.entity)
@ENTITY_ADAPTERS.register(cover.DOMAIN)
class _CoverCapabilities(_AlexaEntity):
def default_display_categories(self):
@ -682,17 +756,26 @@ def api_message(request,
return response
def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
def api_error(request,
namespace='Alexa',
error_type='INTERNAL_ERROR',
error_message="",
payload=None):
"""Create a API formatted error response.
Async friendly.
"""
payload = {
'type': error_type,
'message': error_message,
}
payload = payload or {}
payload['type'] = error_type
payload['message'] = error_message
return api_message(request, name='ErrorResponse', payload=payload)
_LOGGER.info("Request %s/%s error %s: %s",
request[API_HEADER]['namespace'],
request[API_HEADER]['name'],
error_type, error_message)
return api_message(
request, name='ErrorResponse', namespace=namespace, payload=payload)
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
@ -1104,7 +1187,6 @@ def async_api_select_input(hass, config, request, entity):
else:
msg = 'failed to map input {} to a media source on {}'.format(
media_input, entity.entity_id)
_LOGGER.error(msg)
return api_error(
request, error_type='INVALID_VALUE', error_message=msg)
@ -1276,6 +1358,149 @@ def async_api_previous(hass, config, request, entity):
return api_message(request)
def api_error_temp_range(request, temp, min_temp, max_temp, unit):
"""Create temperature value out of range API error response.
Async friendly.
"""
temp_range = {
'minimumValue': {
'value': min_temp,
'scale': API_TEMP_UNITS[unit],
},
'maximumValue': {
'value': max_temp,
'scale': API_TEMP_UNITS[unit],
},
}
msg = 'The requested temperature {} is out of range'.format(temp)
return api_error(
request,
error_type='TEMPERATURE_VALUE_OUT_OF_RANGE',
error_message=msg,
payload={'validRange': temp_range},
)
def temperature_from_object(temp_obj, to_unit, interval=False):
"""Get temperature from Temperature object in requested unit."""
from_unit = TEMP_CELSIUS
temp = float(temp_obj['value'])
if temp_obj['scale'] == 'FAHRENHEIT':
from_unit = TEMP_FAHRENHEIT
elif temp_obj['scale'] == 'KELVIN':
# convert to Celsius if absolute temperature
if not interval:
temp -= 273.15
return convert_temperature(temp, from_unit, to_unit, interval)
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
@extract_entity
async def async_api_set_target_temp(hass, config, request, entity):
"""Process a set target temperature request."""
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
data = {
ATTR_ENTITY_ID: entity.entity_id
}
payload = request[API_PAYLOAD]
if 'targetSetpoint' in payload:
temp = temperature_from_object(
payload['targetSetpoint'], unit)
if temp < min_temp or temp > max_temp:
return api_error_temp_range(
request, temp, min_temp, max_temp, unit)
data[ATTR_TEMPERATURE] = temp
if 'lowerSetpoint' in payload:
temp_low = temperature_from_object(
payload['lowerSetpoint'], unit)
if temp_low < min_temp or temp_low > max_temp:
return api_error_temp_range(
request, temp_low, min_temp, max_temp, unit)
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
if 'upperSetpoint' in payload:
temp_high = temperature_from_object(
payload['upperSetpoint'], unit)
if temp_high < min_temp or temp_high > max_temp:
return api_error_temp_range(
request, temp_high, min_temp, max_temp, unit)
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
return api_message(request)
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
@extract_entity
async def async_api_adjust_target_temp(hass, config, request, entity):
"""Process an adjust target temperature request."""
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
temp_delta = temperature_from_object(
request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True)
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
if target_temp < min_temp or target_temp > max_temp:
return api_error_temp_range(
request, target_temp, min_temp, max_temp, unit)
data = {
ATTR_ENTITY_ID: entity.entity_id,
ATTR_TEMPERATURE: target_temp,
}
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
return api_message(request)
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
@extract_entity
async def async_api_set_thermostat_mode(hass, config, request, entity):
"""Process a set thermostat mode request."""
mode = request[API_PAYLOAD]['thermostatMode']
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
# Work around a pylint false positive due to
# https://github.com/PyCQA/pylint/issues/1830
# pylint: disable=stop-iteration-return
ha_mode = next(
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
None
)
if ha_mode not in operation_list:
msg = 'The requested thermostat mode {} is not supported'.format(mode)
return api_error(
request,
namespace='Alexa.ThermostatController',
error_type='UNSUPPORTED_THERMOSTAT_MODE',
error_message=msg
)
data = {
ATTR_ENTITY_ID: entity.entity_id,
climate.ATTR_OPERATION_MODE: ha_mode,
}
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
blocking=False)
return api_message(request)
@HANDLERS.register(('Alexa', 'ReportState'))
@extract_entity
@asyncio.coroutine

View File

@ -10,14 +10,15 @@ from datetime import timedelta
import aiohttp
import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout
from requests.exceptions import ConnectionError as ConnectError
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['amcrest==1.2.1']
REQUIREMENTS = ['amcrest==1.2.2']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)
@ -63,6 +64,12 @@ SENSORS = {
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
}
# Switch types are defined like: Name, icon
SWITCHES = {
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
'motion_recording': ['Motion Recording', 'mdi:record-rec']
}
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_HOST): cv.string,
@ -81,6 +88,8 @@ CONFIG_SCHEMA = vol.Schema({
cv.time_period,
vol.Optional(CONF_SENSORS):
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
vol.Optional(CONF_SWITCHES):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
})])
}, extra=vol.ALLOW_EXTRA)
@ -93,14 +102,15 @@ def setup(hass, config):
amcrest_cams = config[DOMAIN]
for device in amcrest_cams:
camera = AmcrestCamera(device.get(CONF_HOST),
device.get(CONF_PORT),
device.get(CONF_USERNAME),
device.get(CONF_PASSWORD)).camera
try:
camera = AmcrestCamera(device.get(CONF_HOST),
device.get(CONF_PORT),
device.get(CONF_USERNAME),
device.get(CONF_PASSWORD)).camera
# pylint: disable=pointless-statement
camera.current_time
except (ConnectTimeout, HTTPError) as ex:
except (ConnectError, ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}<br />'
@ -108,12 +118,13 @@ def setup(hass, config):
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
continue
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
name = device.get(CONF_NAME)
resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)]
sensors = device.get(CONF_SENSORS)
switches = device.get(CONF_SWITCHES)
stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)]
username = device.get(CONF_USERNAME)
@ -143,6 +154,13 @@ def setup(hass, config):
CONF_SENSORS: sensors,
}, config)
if switches:
discovery.load_platform(
hass, 'switch', DOMAIN, {
CONF_NAME: name,
CONF_SWITCHES: switches
}, config)
return True

View File

@ -52,9 +52,8 @@ def setup(hass, config):
hass.http.register_view(APIComponentsView)
hass.http.register_view(APITemplateView)
log_path = hass.data.get(DATA_LOGGING, None)
if log_path:
hass.http.register_static_path(URL_API_ERROR_LOG, log_path, False)
if DATA_LOGGING in hass.data:
hass.http.register_view(APIErrorLog)
return True
@ -356,6 +355,17 @@ class APITemplateView(HomeAssistantView):
HTTP_BAD_REQUEST)
class APIErrorLog(HomeAssistantView):
"""View to fetch the error log."""
url = URL_API_ERROR_LOG
name = "api:error_log"
async def get(self, request):
"""Retrieve API error log."""
return await self.file(request, request.app['hass'].data[DATA_LOGGING])
@asyncio.coroutine
def async_services_json(hass):
"""Generate services data to JSONify."""

View File

@ -7,8 +7,8 @@ https://home-assistant.io/components/binary_sensor.bmw_connected_drive/
import asyncio
import logging
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
DEPENDENCIES = ['bmw_connected_drive']
@ -45,7 +45,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
self._account = account
self._vehicle = vehicle
self._attribute = attribute
self._name = '{} {}'.format(self._vehicle.modelName, self._attribute)
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._sensor_name = sensor_name
self._device_class = device_class
self._state = None
@ -75,7 +75,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
"""Return the state attributes of the binary sensor."""
vehicle_state = self._vehicle.state
result = {
'car': self._vehicle.modelName
'car': self._vehicle.name
}
if self._attribute == 'lids':
@ -91,6 +91,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
def update(self):
"""Read new state data from the library."""
from bimmer_connected.state import LockState
vehicle_state = self._vehicle.state
# device class opening: On means open, Off means closed
@ -101,9 +102,9 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
self._state = not vehicle_state.all_windows_closed
# device class safety: On means unsafe, Off means safe
if self._attribute == 'door_lock_state':
# Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED
self._state = bool(vehicle_state.door_lock_state.value
in ('SELECTIVELOCKED', 'UNLOCKED'))
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
self._state = vehicle_state.door_lock_state not in \
[LockState.LOCKED, LockState.SECURED]
def update_callback(self):
"""Schedule a state update."""

View File

@ -1,97 +0,0 @@
"""
Support for Mercedes cars with Mercedes ME.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.mercedesme/
"""
import logging
import datetime
from homeassistant.components.binary_sensor import (BinarySensorDevice)
from homeassistant.components.mercedesme import (
DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, BINARY_SENSORS)
DEPENDENCIES = ['mercedesme']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the sensor platform."""
data = hass.data[DATA_MME].data
if not data.cars:
_LOGGER.error("No cars found. Check component log.")
return
devices = []
for car in data.cars:
for key, value in sorted(BINARY_SENSORS.items()):
if car['availabilities'].get(key, 'INVALID') == 'VALID':
devices.append(MercedesMEBinarySensor(
data, key, value[0], car["vin"], None))
else:
_LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"])
add_devices(devices, True)
class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice):
"""Representation of a Sensor."""
@property
def is_on(self):
"""Return the state of the binary sensor."""
return self._state
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._internal_name == "windowsClosed":
return {
"window_front_left": self._car["windowStatusFrontLeft"],
"window_front_right": self._car["windowStatusFrontRight"],
"window_rear_left": self._car["windowStatusRearLeft"],
"window_rear_right": self._car["windowStatusRearRight"],
"original_value": self._car[self._internal_name],
"last_update": datetime.datetime.fromtimestamp(
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
"car": self._car["license"]
}
elif self._internal_name == "tireWarningLight":
return {
"front_right_tire_pressure_kpa":
self._car["frontRightTirePressureKpa"],
"front_left_tire_pressure_kpa":
self._car["frontLeftTirePressureKpa"],
"rear_right_tire_pressure_kpa":
self._car["rearRightTirePressureKpa"],
"rear_left_tire_pressure_kpa":
self._car["rearLeftTirePressureKpa"],
"original_value": self._car[self._internal_name],
"last_update": datetime.datetime.fromtimestamp(
self._car["lastUpdate"]
).strftime('%Y-%m-%d %H:%M:%S'),
"car": self._car["license"],
}
return {
"original_value": self._car[self._internal_name],
"last_update": datetime.datetime.fromtimestamp(
self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
"car": self._car["license"]
}
def update(self):
"""Fetch new state data for the sensor."""
self._car = next(
car for car in self._data.cars if car["vin"] == self._vin)
if self._internal_name == "windowsClosed":
self._state = bool(self._car[self._internal_name] == "CLOSED")
elif self._internal_name == "tireWarningLight":
self._state = bool(self._car[self._internal_name] != "INACTIVE")
else:
self._state = self._car[self._internal_name] is True
_LOGGER.debug("Updated %s Value: %s IsOn: %s",
self._internal_name, self._state, self.is_on)

View File

@ -21,11 +21,12 @@ SENSORS = {
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MySensors platform for binary sensors."""
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the mysensors platform for binary sensors."""
mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsBinarySensor,
add_devices=add_devices)
async_add_devices=async_add_devices)
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice):

View File

@ -30,8 +30,8 @@ ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada',
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
'Sweden', 'SE', 'UnitedKingdom', 'UK', 'UnitedStates', 'US',
'Wales']
'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK',
'UnitedStates', 'US', 'Wales']
CONF_COUNTRY = 'country'
CONF_PROVINCE = 'province'
CONF_WORKDAYS = 'workdays'
@ -47,13 +47,13 @@ DEFAULT_OFFSET = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
vol.Optional(CONF_PROVINCE): cv.string,
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int),
vol.Optional(CONF_PROVINCE): cv.string,
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
})
@ -74,14 +74,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if province:
# 'state' and 'prov' are not interchangeable, so need to make
# sure we use the right one
if (hasattr(obj_holidays, "PROVINCES") and
if (hasattr(obj_holidays, 'PROVINCES') and
province in obj_holidays.PROVINCES):
obj_holidays = getattr(holidays, country)(prov=province,
years=year)
elif (hasattr(obj_holidays, "STATES") and
obj_holidays = getattr(holidays, country)(
prov=province, years=year)
elif (hasattr(obj_holidays, 'STATES') and
province in obj_holidays.STATES):
obj_holidays = getattr(holidays, country)(state=province,
years=year)
obj_holidays = getattr(holidays, country)(
state=province, years=year)
else:
_LOGGER.error("There is no province/state %s in country %s",
province, country)

View File

@ -4,30 +4,29 @@ Reads vehicle status from BMW connected drive portal.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/bmw_connected_drive/
"""
import logging
import datetime
import logging
import voluptuous as vol
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD
)
REQUIREMENTS = ['bimmer_connected==0.4.1']
REQUIREMENTS = ['bimmer_connected==0.5.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'bmw_connected_drive'
CONF_VALUES = 'values'
CONF_COUNTRY = 'country'
CONF_REGION = 'region'
ACCOUNT_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_COUNTRY): cv.string,
vol.Required(CONF_REGION): vol.Any('north_america', 'china',
'rest_of_world'),
})
CONFIG_SCHEMA = vol.Schema({
@ -47,9 +46,9 @@ def setup(hass, config):
for name, account_config in config[DOMAIN].items():
username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD]
country = account_config[CONF_COUNTRY]
region = account_config[CONF_REGION]
_LOGGER.debug('Adding new account %s', name)
bimmer = BMWConnectedDriveAccount(username, password, country, name)
bimmer = BMWConnectedDriveAccount(username, password, region, name)
accounts.append(bimmer)
# update every UPDATE_INTERVAL minutes, starting now
@ -75,12 +74,15 @@ def setup(hass, config):
class BMWConnectedDriveAccount(object):
"""Representation of a BMW vehicle."""
def __init__(self, username: str, password: str, country: str,
def __init__(self, username: str, password: str, region_str: str,
name: str) -> None:
"""Constructor."""
from bimmer_connected.account import ConnectedDriveAccount
from bimmer_connected.country_selector import get_region_from_name
self.account = ConnectedDriveAccount(username, password, country)
region = get_region_from_name(region_str)
self.account = ConnectedDriveAccount(username, password, region)
self.name = name
self._update_listeners = []

View File

@ -19,7 +19,6 @@ from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_TOPIC = 'topic'
DEFAULT_NAME = 'MQTT Camera'
DEPENDENCIES = ['mqtt']
@ -33,9 +32,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the MQTT Camera."""
topic = config[CONF_TOPIC]
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
async_add_devices([MqttCamera(config[CONF_NAME], topic)])
async_add_devices([MqttCamera(
config.get(CONF_NAME),
config.get(CONF_TOPIC)
)])
class MqttCamera(Camera):

View File

@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.helpers import discovery
from homeassistant.util import Throttle
REQUIREMENTS = ['py-canary==0.4.1']
REQUIREMENTS = ['py-canary==0.5.0']
_LOGGER = logging.getLogger(__name__)

View File

@ -31,10 +31,12 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_OPERATION_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MySensors climate."""
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the mysensors climate."""
mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices)
hass, DOMAIN, discovery_info, MySensorsHVAC,
async_add_devices=async_add_devices)
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
@ -163,8 +165,8 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
self._values[self.value_type] = operation_mode
self.schedule_update_ha_state()
def update(self):
async def async_update(self):
"""Update the controller with the latest value from a sensor."""
super().update()
await super().async_update()
self._values[self.value_type] = DICT_MYS_TO_HA[
self._values[self.value_type]]

View File

@ -179,7 +179,7 @@ class NestThermostat(ClimateDevice):
try:
self.device.target = temp
except nest.nest.APIError:
_LOGGER.error("An error occured while setting the temperature")
_LOGGER.error("An error occurred while setting the temperature")
def set_operation_mode(self, operation_mode):
"""Set operation mode."""

View File

@ -14,24 +14,16 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
'entity_registry')
'entity_registry', 'config_entries')
ON_DEMAND = ('zwave',)
FEATURE_FLAGS = ('config_entries',)
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the config component."""
global SECTIONS
yield from hass.components.frontend.async_register_built_in_panel(
'config', 'config', 'mdi:settings')
# Temporary way of allowing people to opt-in for unreleased config sections
for key, value in config.get(DOMAIN, {}).items():
if key in FEATURE_FLAGS and value:
SECTIONS += (key,)
@asyncio.coroutine
def setup_panel(panel_name):
"""Set up a panel."""

View File

@ -1,24 +0,0 @@
{
"config": {
"error": {
"invalid_object_id": "Ung\u00fcltige Objekt-ID"
},
"step": {
"init": {
"data": {
"object_id": "Objekt-ID"
},
"description": "Bitte gib eine Objekt_ID f\u00fcr das Test-Entity ein.",
"title": "W\u00e4hle eine Objekt-ID"
},
"name": {
"data": {
"name": "Name"
},
"description": "Bitte gib einen Namen f\u00fcr das Test-Entity ein",
"title": "Name des Test-Entity"
}
},
"title": "Beispiel Konfig-Eintrag"
}
}

View File

@ -1,24 +0,0 @@
{
"config": {
"error": {
"invalid_object_id": "Invalid object ID"
},
"step": {
"init": {
"data": {
"object_id": "Object ID"
},
"description": "Please enter an object_id for the test entity.",
"title": "Pick object id"
},
"name": {
"data": {
"name": "Name"
},
"description": "Please enter a name for the test entity.",
"title": "Name of the entity"
}
},
"title": "Config Entry Example"
}
}

View File

@ -1,11 +0,0 @@
{
"config": {
"step": {
"name": {
"data": {
"name": "Nimi"
}
}
}
}
}

View File

@ -1,24 +0,0 @@
{
"config": {
"error": {
"invalid_object_id": "\uc624\ube0c\uc81d\ud2b8 ID\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
"step": {
"init": {
"data": {
"object_id": "\uc624\ube0c\uc81d\ud2b8 ID"
},
"description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc624\ube0c\uc81d\ud2b8 ID \ub97c \uc785\ub825\ud558\uc138\uc694",
"title": "\uc624\ube0c\uc81d\ud2b8 ID \uc120\ud0dd"
},
"name": {
"data": {
"name": "\uc774\ub984"
},
"description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984\uc744 \uc785\ub825\ud558\uc138\uc694.",
"title": "\uad6c\uc131\uc694\uc18c \uc774\ub984"
}
},
"title": "\uc785\ub825 \uc608\uc81c \uad6c\uc131"
}
}

View File

@ -1,24 +0,0 @@
{
"config": {
"error": {
"invalid_object_id": "Ongeldig object ID"
},
"step": {
"init": {
"data": {
"object_id": "Object ID"
},
"description": "Voer een object_id in voor het testen van de entiteit.",
"title": "Kies object id"
},
"name": {
"data": {
"name": "Naam"
},
"description": "Voer een naam in voor het testen van de entiteit.",
"title": "Naam van de entiteit"
}
},
"title": "Voorbeeld van de config vermelding"
}
}

View File

@ -1,24 +0,0 @@
{
"config": {
"error": {
"invalid_object_id": "Ugyldig objekt ID"
},
"step": {
"init": {
"data": {
"object_id": "Objekt ID"
},
"description": "Vennligst skriv inn en object_id for testenheten.",
"title": "Velg objekt ID"
},
"name": {
"data": {
"name": "Navn"
},
"description": "Vennligst skriv inn et navn for testenheten.",
"title": "Navn p\u00e5 enheten"
}
},
"title": "Konfigureringseksempel"
}
}

View File

@ -1,24 +0,0 @@
{
"config": {
"error": {
"invalid_object_id": "Nieprawid\u0142owy identyfikator obiektu"
},
"step": {
"init": {
"data": {
"object_id": "Identyfikator obiektu"
},
"description": "Prosz\u0119 wprowadzi\u0107 identyfikator obiektu (object_id) dla jednostki testowej.",
"title": "Wybierz identyfikator obiektu"
},
"name": {
"data": {
"name": "Nazwa"
},
"description": "Prosz\u0119 wprowadzi\u0107 nazw\u0119 dla jednostki testowej.",
"title": "Nazwa jednostki"
}
},
"title": "Przyk\u0142ad wpisu do konfiguracji"
}
}

View File

@ -1,15 +0,0 @@
{
"config": {
"step": {
"init": {
"description": "Introduce\u021bi un obiect_id pentru entitatea testat\u0103.",
"title": "Alege\u021bi id-ul obiectului"
},
"name": {
"data": {
"name": "Nume"
}
}
}
}
}

View File

@ -1,24 +0,0 @@
{
"config": {
"error": {
"invalid_object_id": "Neveljaven ID objekta"
},
"step": {
"init": {
"data": {
"object_id": "ID objekta"
},
"description": "Prosimo, vnesite Id_objekta za testni subjekt.",
"title": "Izberite ID objekta"
},
"name": {
"data": {
"name": "Ime"
},
"description": "Vnesite ime za testni subjekt.",
"title": "Ime subjekta"
}
},
"title": "Primer nastavitve"
}
}

View File

@ -1,24 +0,0 @@
{
"config": {
"error": {
"invalid_object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng kh\u00f4ng h\u1ee3p l\u1ec7"
},
"step": {
"init": {
"data": {
"object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng"
},
"description": "Xin vui l\u00f2ng nh\u1eadp m\u1ed9t object_id cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.",
"title": "Ch\u1ecdn id \u0111\u1ed1i t\u01b0\u1ee3ng"
},
"name": {
"data": {
"name": "T\u00ean"
},
"description": "Xin vui l\u00f2ng nh\u1eadp t\u00ean cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.",
"title": "T\u00ean c\u1ee7a th\u1ef1c th\u1ec3"
}
},
"title": "V\u00ed d\u1ee5 v\u1ec1 c\u1ea5u h\u00ecnh th\u1ef1c th\u1ec3"
}
}

View File

@ -1,24 +0,0 @@
{
"config": {
"error": {
"invalid_object_id": "\u65e0\u6548\u7684\u5bf9\u8c61 ID"
},
"step": {
"init": {
"data": {
"object_id": "\u5bf9\u8c61 ID"
},
"description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u5bf9\u8c61 ID",
"title": "\u8bf7\u9009\u62e9\u5bf9\u8c61 ID"
},
"name": {
"data": {
"name": "\u540d\u79f0"
},
"description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u540d\u79f0",
"title": "\u8bbe\u5907\u540d\u79f0"
}
},
"title": "\u6837\u4f8b\u914d\u7f6e\u6761\u76ee"
}
}

View File

@ -1,98 +0,0 @@
"""Example component to show how config entries work."""
import asyncio
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.util import slugify
DOMAIN = 'config_entry_example'
@asyncio.coroutine
def async_setup(hass, config):
"""Setup for our example component."""
return True
@asyncio.coroutine
def async_setup_entry(hass, entry):
"""Initialize an entry."""
entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
hass.states.async_set(entity_id, 'loaded', {
ATTR_FRIENDLY_NAME: entry.data['name']
})
# Indicate setup was successful.
return True
@asyncio.coroutine
def async_unload_entry(hass, entry):
"""Unload an entry."""
entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
hass.states.async_remove(entity_id)
# Indicate unload was successful.
return True
@config_entries.HANDLERS.register(DOMAIN)
class ExampleConfigFlow(config_entries.ConfigFlowHandler):
"""Handle an example configuration flow."""
VERSION = 1
def __init__(self):
"""Initialize a Hue config handler."""
self.object_id = None
@asyncio.coroutine
def async_step_init(self, user_input=None):
"""Start config flow."""
errors = None
if user_input is not None:
object_id = user_input['object_id']
if object_id != '' and object_id == slugify(object_id):
self.object_id = user_input['object_id']
return (yield from self.async_step_name())
errors = {
'object_id': 'invalid_object_id'
}
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({
'object_id': str
}),
errors=errors
)
@asyncio.coroutine
def async_step_name(self, user_input=None):
"""Ask user to enter the name."""
errors = None
if user_input is not None:
name = user_input['name']
if name != '':
return self.async_create_entry(
title=name,
data={
'name': name,
'object_id': self.object_id,
}
)
return self.async_show_form(
step_id='name',
data_schema=vol.Schema({
'name': str
}),
errors=errors
)

View File

@ -1,24 +0,0 @@
{
"config": {
"title": "Config Entry Example",
"step": {
"init": {
"title": "Pick object id",
"description": "Please enter an object_id for the test entity.",
"data": {
"object_id": "Object ID"
}
},
"name": {
"title": "Name of the entity",
"description": "Please enter a name for the test entity.",
"data": {
"name": "Name"
}
}
},
"error": {
"invalid_object_id": "Invalid object ID"
}
}
}

View File

@ -13,10 +13,14 @@ from homeassistant import core
from homeassistant.components import http
from homeassistant.components.http.data_validator import (
RequestDataValidator)
from homeassistant.components.cover import (INTENT_OPEN_COVER,
INTENT_CLOSE_COVER)
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import intent
from homeassistant.loader import bind_hass
from homeassistant.setup import (ATTR_COMPONENT)
_LOGGER = logging.getLogger(__name__)
@ -28,6 +32,13 @@ DOMAIN = 'conversation'
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REGEX_TYPE = type(re.compile(''))
UTTERANCES = {
'cover': {
INTENT_OPEN_COVER: ['Open [the] [a] [an] {name}[s]'],
INTENT_CLOSE_COVER: ['Close [the] [a] [an] {name}[s]']
}
}
SERVICE_PROCESS = 'process'
SERVICE_PROCESS_SCHEMA = vol.Schema({
@ -112,6 +123,25 @@ async def async_setup(hass, config):
'[the] [a] [an] {name}[s] toggle',
])
@callback
def register_utterances(component):
"""Register utterances for a component."""
if component not in UTTERANCES:
return
for intent_type, sentences in UTTERANCES[component].items():
async_register(hass, intent_type, sentences)
@callback
def component_loaded(event):
"""Handle a new component loaded."""
register_utterances(event.data[ATTR_COMPONENT])
hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded)
# Check already loaded components.
for component in hass.config.components:
register_utterances(component)
return True

View File

@ -17,6 +17,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv
from homeassistant.components import group
from homeassistant.helpers import intent
from homeassistant.const import (
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
@ -55,6 +56,9 @@ ATTR_CURRENT_TILT_POSITION = 'current_tilt_position'
ATTR_POSITION = 'position'
ATTR_TILT_POSITION = 'tilt_position'
INTENT_OPEN_COVER = 'HassOpenCover'
INTENT_CLOSE_COVER = 'HassCloseCover'
COVER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
@ -181,6 +185,12 @@ async def async_setup(hass, config):
hass.services.async_register(
DOMAIN, service_name, async_handle_cover_service,
schema=schema)
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER,
"Opened {}"))
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER,
"Closed {}"))
return True

View File

@ -0,0 +1,120 @@
"""
Support for Gogogate2 Garage Doors.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.gogogate2/
"""
import logging
import voluptuous as vol
from homeassistant.components.cover import (
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, STATE_UNKNOWN,
CONF_IP_ADDRESS, CONF_NAME)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pygogogate2==0.0.3']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'gogogate2'
NOTIFICATION_ID = 'gogogate2_notification'
NOTIFICATION_TITLE = 'Gogogate2 Cover Setup'
COVER_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Gogogate2 component."""
from pygogogate2 import Gogogate2API as pygogogate2
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
ip_address = config.get(CONF_IP_ADDRESS)
name = config.get(CONF_NAME)
mygogogate2 = pygogogate2(username, password, ip_address)
try:
devices = mygogogate2.get_devices()
if devices is False:
raise ValueError(
"Username or Password is incorrect or no devices found")
add_devices(MyGogogate2Device(
mygogogate2, door, name) for door in devices)
return
except (TypeError, KeyError, NameError, ValueError) as ex:
_LOGGER.error("%s", ex)
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return
class MyGogogate2Device(CoverDevice):
"""Representation of a Gogogate2 cover."""
def __init__(self, mygogogate2, device, name):
"""Initialize with API object, device id."""
self.mygogogate2 = mygogogate2
self.device_id = device['door']
self._name = name or device['name']
self._status = device['status']
self.available = None
@property
def name(self):
"""Return the name of the garage door if any."""
return self._name if self._name else DEFAULT_NAME
@property
def is_closed(self):
"""Return true if cover is closed, else False."""
return self._status == STATE_CLOSED
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'garage'
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_OPEN | SUPPORT_CLOSE
@property
def available(self):
"""Could the device be accessed during the last update call."""
return self.available
def close_cover(self, **kwargs):
"""Issue close command to cover."""
self.mygogogate2.close_device(self.device_id)
self.schedule_update_ha_state(True)
def open_cover(self, **kwargs):
"""Issue open command to cover."""
self.mygogogate2.open_device(self.device_id)
self.schedule_update_ha_state(True)
def update(self):
"""Update status of cover."""
try:
self._status = self.mygogogate2.get_status(self.device_id)
self.available = True
except (TypeError, KeyError, NameError, ValueError) as ex:
_LOGGER.error("%s", ex)
self._status = STATE_UNKNOWN
self.available = False

View File

@ -9,10 +9,12 @@ from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice
from homeassistant.const import STATE_OFF, STATE_ON
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MySensors platform for covers."""
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the mysensors platform for covers."""
mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsCover, add_devices=add_devices)
hass, DOMAIN, discovery_info, MySensorsCover,
async_add_devices=async_add_devices)
class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):

View File

@ -87,7 +87,7 @@ class RPiGPIOCover(CoverDevice):
self._invert_relay = invert_relay
rpi_gpio.setup_output(self._relay_pin)
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1)
@property
def name(self):
@ -105,9 +105,9 @@ class RPiGPIOCover(CoverDevice):
def _trigger(self):
"""Trigger the cover."""
rpi_gpio.write_output(self._relay_pin, self._invert_relay)
rpi_gpio.write_output(self._relay_pin, 1 if self._invert_relay else 0)
sleep(self._relay_time)
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1)
def close_cover(self, **kwargs):
"""Close the cover."""

View File

@ -0,0 +1,25 @@
{
"config": {
"title": "deCONZ",
"step": {
"init": {
"title": "Define deCONZ gateway",
"data": {
"host": "Host",
"port": "Port (default value: '80')"
}
},
"link": {
"title": "Link with deCONZ",
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
}
},
"error": {
"no_key": "Couldn't get an API key"
},
"abort": {
"no_bridges": "No deCONZ bridges discovered",
"one_instance_only": "Component only supports one deCONZ instance"
}
}
}

View File

@ -8,16 +8,17 @@ import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.discovery import SERVICE_DECONZ
from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers import discovery, aiohttp_client
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['pydeconz==32']
REQUIREMENTS = ['pydeconz==35']
_LOGGER = logging.getLogger(__name__)
@ -160,7 +161,8 @@ async def async_request_configuration(hass, config, deconz_config):
async def async_configuration_callback(data):
"""Set up actions to do when our configuration callback is called."""
from pydeconz.utils import async_get_api_key
api_key = await async_get_api_key(hass.loop, **deconz_config)
websession = async_get_clientsession(hass)
api_key = await async_get_api_key(websession, **deconz_config)
if api_key:
deconz_config[CONF_API_KEY] = api_key
result = await async_setup_deconz(hass, config, deconz_config)
@ -186,3 +188,85 @@ async def async_request_configuration(hass, config, deconz_config):
entity_picture="/static/images/logo_deconz.jpeg",
submit_caption="I have unlocked the gateway",
)
@config_entries.HANDLERS.register(DOMAIN)
class DeconzFlowHandler(config_entries.ConfigFlowHandler):
"""Handle a deCONZ config flow."""
VERSION = 1
def __init__(self):
"""Initialize the deCONZ flow."""
self.bridges = []
self.deconz_config = {}
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
from pydeconz.utils import async_discovery
if DOMAIN in self.hass.data:
return self.async_abort(
reason='one_instance_only'
)
if user_input is not None:
for bridge in self.bridges:
if bridge[CONF_HOST] == user_input[CONF_HOST]:
self.deconz_config = bridge
return await self.async_step_link()
session = aiohttp_client.async_get_clientsession(self.hass)
self.bridges = await async_discovery(session)
if len(self.bridges) == 1:
self.deconz_config = self.bridges[0]
return await self.async_step_link()
elif len(self.bridges) > 1:
hosts = []
for bridge in self.bridges:
hosts.append(bridge[CONF_HOST])
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({
vol.Required(CONF_HOST): vol.In(hosts)
})
)
return self.async_abort(
reason='no_bridges'
)
async def async_step_link(self, user_input=None):
"""Attempt to link with the deCONZ bridge."""
from pydeconz.utils import async_get_api_key
errors = {}
if user_input is not None:
session = aiohttp_client.async_get_clientsession(self.hass)
api_key = await async_get_api_key(session, **self.deconz_config)
if api_key:
self.deconz_config[CONF_API_KEY] = api_key
return self.async_create_entry(
title='deCONZ',
data=self.deconz_config
)
else:
errors['base'] = 'no_key'
return self.async_show_form(
step_id='link',
errors=errors,
)
async def async_setup_entry(hass, entry):
"""Set up a bridge for a config entry."""
if DOMAIN in hass.data:
_LOGGER.error(
"Config entry failed since one deCONZ instance already exists")
return False
result = await async_setup_deconz(hass, None, entry.data)
if result:
return True
return False

View File

@ -0,0 +1,25 @@
{
"config": {
"title": "deCONZ",
"step": {
"init": {
"title": "Define deCONZ gateway",
"data": {
"host": "Host",
"port": "Port (default value: '80')"
}
},
"link": {
"title": "Link with deCONZ",
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
}
},
"error": {
"no_key": "Couldn't get an API key"
},
"abort": {
"no_bridges": "No deCONZ bridges discovered",
"one_instance_only": "Component only supports one deCONZ instance"
}
}
}

View File

@ -9,8 +9,6 @@ from datetime import timedelta
import logging
from typing import Any, List, Sequence, Callable
import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.setup import async_prepare_setup_platform
@ -19,7 +17,6 @@ from homeassistant.loader import bind_hass
from homeassistant.components import group, zone
from homeassistant.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
@ -76,7 +73,6 @@ ATTR_LOCATION_NAME = 'location_name'
ATTR_MAC = 'mac'
ATTR_NAME = 'name'
ATTR_SOURCE_TYPE = 'source_type'
ATTR_VENDOR = 'vendor'
ATTR_CONSIDER_HOME = 'consider_home'
SOURCE_TYPE_GPS = 'gps'
@ -328,14 +324,10 @@ class DeviceTracker(object):
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id])
# lookup mac vendor string to be stored in config
yield from device.set_vendor_for_mac()
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name,
ATTR_MAC: device.mac,
ATTR_VENDOR: device.vendor,
})
# update known_devices.yaml
@ -413,7 +405,6 @@ class Device(Entity):
consider_home = None # type: dt_util.dt.timedelta
battery = None # type: int
attributes = None # type: dict
vendor = None # type: str
icon = None # type: str
# Track if the last update of this device was HOME.
@ -423,7 +414,7 @@ class Device(Entity):
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str = None,
picture: str = None, gravatar: str = None, icon: str = None,
hide_if_away: bool = False, vendor: str = None) -> None:
hide_if_away: bool = False) -> None:
"""Initialize a device."""
self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@ -451,7 +442,6 @@ class Device(Entity):
self.icon = icon
self.away_hide = hide_if_away
self.vendor = vendor
self.source_type = None
@ -567,51 +557,6 @@ class Device(Entity):
self._state = STATE_HOME
self.last_update_home = True
@asyncio.coroutine
def set_vendor_for_mac(self):
"""Set vendor string using api.macvendors.com."""
self.vendor = yield from self.get_vendor_for_mac()
@asyncio.coroutine
def get_vendor_for_mac(self):
"""Try to find the vendor string for a given MAC address."""
if not self.mac:
return None
if '_' in self.mac:
_, mac = self.mac.split('_', 1)
else:
mac = self.mac
if not len(mac.split(':')) == 6:
return 'unknown'
# We only need the first 3 bytes of the MAC for a lookup
# this improves somewhat on privacy
oui_bytes = mac.split(':')[0:3]
# bytes like 00 get truncates to 0, API needs full bytes
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
url = 'http://api.macvendors.com/' + oui
try:
websession = async_get_clientsession(self.hass)
with async_timeout.timeout(5, loop=self.hass.loop):
resp = yield from websession.get(url)
# mac vendor found, response is the string
if resp.status == 200:
vendor_string = yield from resp.text()
return vendor_string
# If vendor is not known to the API (404) or there
# was a failure during the lookup (500); set vendor
# to something other then None to prevent retry
# as the value is only relevant when it is to be stored
# in the 'known_devices.yaml' file which only happens
# the first time the device is seen.
return 'unknown'
except (asyncio.TimeoutError, aiohttp.ClientError):
# Same as above
return 'unknown'
@asyncio.coroutine
def async_added_to_hass(self):
"""Add an entity."""
@ -685,7 +630,6 @@ def async_load_config(path: str, hass: HomeAssistantType,
vol.Optional('picture', default=None): vol.Any(None, cv.string),
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
cv.time_period, cv.positive_timedelta),
vol.Optional('vendor', default=None): vol.Any(None, cv.string),
})
try:
result = []
@ -697,6 +641,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
return []
for dev_id, device in devices.items():
# Deprecated option. We just ignore it to avoid breaking change
device.pop('vendor', None)
try:
device = dev_schema(device)
device['dev_id'] = cv.slugify(dev_id)
@ -772,7 +718,6 @@ def update_config(path: str, dev_id: str, device: Device):
'picture': device.config_picture,
'track': device.track,
CONF_AWAY_HIDE: device.away_hide,
'vendor': device.vendor,
}}
out.write('\n')
out.write(dump(device))

View File

@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_PUB_KEY = 'pub_key'
CONF_SSH_KEY = 'ssh_key'
CONF_REQUIRE_IP = 'require_ip'
DEFAULT_SSH_PORT = 22
SECRET_GROUP = 'Password or SSH Key'
@ -36,6 +37,7 @@ PLATFORM_SCHEMA = vol.All(
vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']),
vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']),
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port,
vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile
@ -115,6 +117,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
self.protocol = config[CONF_PROTOCOL]
self.mode = config[CONF_MODE]
self.port = config[CONF_PORT]
self.require_ip = config[CONF_REQUIRE_IP]
if self.protocol == 'ssh':
self.connection = SshConnection(
@ -172,7 +175,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
ret_devices = {}
for key in devices:
if devices[key].ip is not None:
if not self.require_ip or devices[key].ip is not None:
ret_devices[key] = devices[key]
return ret_devices

View File

@ -17,12 +17,15 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pybluez==0.22']
REQUIREMENTS = ['pybluez==0.22', 'bt_proximity==0.1.2']
BT_PREFIX = 'BT_'
CONF_REQUEST_RSSI = 'request_rssi'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TRACK_NEW): cv.boolean
vol.Optional(CONF_TRACK_NEW): cv.boolean,
vol.Optional(CONF_REQUEST_RSSI): cv.boolean
})
@ -30,11 +33,15 @@ def setup_scanner(hass, config, see, discovery_info=None):
"""Set up the Bluetooth Scanner."""
# pylint: disable=import-error
import bluetooth
from bt_proximity import BluetoothRSSI
def see_device(device):
def see_device(mac, name, rssi=None):
"""Mark a device as seen."""
see(mac=BT_PREFIX + device[0], host_name=device[1],
source_type=SOURCE_TYPE_BLUETOOTH)
attributes = {}
if rssi is not None:
attributes['rssi'] = rssi
see(mac="{}_{}".format(BT_PREFIX, mac), host_name=name,
attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH)
def discover_devices():
"""Discover Bluetooth devices."""
@ -64,27 +71,32 @@ def setup_scanner(hass, config, see, discovery_info=None):
if track_new:
for dev in discover_devices():
if dev[0] not in devs_to_track and \
dev[0] not in devs_donot_track:
dev[0] not in devs_donot_track:
devs_to_track.append(dev[0])
see_device(dev)
see_device(dev[0], dev[1])
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
request_rssi = config.get(CONF_REQUEST_RSSI, False)
def update_bluetooth(now):
"""Lookup Bluetooth device and update status."""
try:
if track_new:
for dev in discover_devices():
if dev[0] not in devs_to_track and \
dev[0] not in devs_donot_track:
dev[0] not in devs_donot_track:
devs_to_track.append(dev[0])
for mac in devs_to_track:
_LOGGER.debug("Scanning %s", mac)
result = bluetooth.lookup_name(mac, timeout=5)
if not result:
rssi = None
if request_rssi:
rssi = BluetoothRSSI(mac).request_rssi()
if result is None:
# Could not lookup device name
continue
see_device((mac, result))
see_device(mac, result, rssi)
except bluetooth.BluetoothError:
_LOGGER.exception("Error looking up Bluetooth device")
track_point_in_utc_time(

View File

@ -36,16 +36,20 @@ class BMWDeviceTracker(object):
self.vehicle = vehicle
def update(self) -> None:
"""Update the device info."""
dev_id = slugify(self.vehicle.modelName)
"""Update the device info.
Only update the state in home assistant if tracking in
the car is enabled.
"""
dev_id = slugify(self.vehicle.name)
if not self.vehicle.state.is_vehicle_tracking_enabled:
_LOGGER.debug('Tracking is disabled for vehicle %s', dev_id)
return
_LOGGER.debug('Updating %s', dev_id)
attrs = {
'trackr_id': dev_id,
'id': dev_id,
'name': self.vehicle.modelName
}
self._see(
dev_id=dev_id, host_name=self.vehicle.modelName,
gps=self.vehicle.state.gps_position, attributes=attrs,
icon='mdi:car'
dev_id=dev_id, host_name=self.vehicle.name,
gps=self.vehicle.state.gps_position, icon='mdi:car'
)

View File

@ -0,0 +1,83 @@
"""
Support for Google Maps location sharing.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.google_maps/
"""
import logging
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, SOURCE_TYPE_GPS)
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['locationsharinglib==0.4.0']
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
})
def setup_scanner(hass, config: ConfigType, see, discovery_info=None):
"""Set up the scanner."""
scanner = GoogleMapsScanner(hass, config, see)
return scanner.success_init
class GoogleMapsScanner(object):
"""Representation of an Google Maps location sharing account."""
def __init__(self, hass, config: ConfigType, see) -> None:
"""Initialize the scanner."""
from locationsharinglib import Service
from locationsharinglib.locationsharinglibexceptions import InvalidUser
self.see = see
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
try:
self.service = Service(self.username, self.password,
hass.config.path(CREDENTIALS_FILE))
self._update_info()
track_time_interval(
hass, self._update_info, MIN_TIME_BETWEEN_SCANS)
self.success_init = True
except InvalidUser:
_LOGGER.error('You have specified invalid login credentials')
self.success_init = False
def _update_info(self, now=None):
for person in self.service.get_all_people():
dev_id = 'google_maps_{0}'.format(slugify(person.id))
attrs = {
'id': person.id,
'nickname': person.nickname,
'full_name': person.full_name,
'last_seen': person.datetime,
'address': person.address
}
self.see(
dev_id=dev_id,
gps=(person.latitude, person.longitude),
picture=person.picture_url,
source_type=SOURCE_TYPE_GPS,
attributes=attrs
)

View File

@ -1,74 +0,0 @@
"""
Support for Mercedes cars with Mercedes ME.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/device_tracker.mercedesme/
"""
import logging
from datetime import timedelta
from homeassistant.components.mercedesme import DATA_MME
from homeassistant.helpers.event import track_time_interval
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['mercedesme']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
def setup_scanner(hass, config, see, discovery_info=None):
"""Set up the Mercedes ME tracker."""
if discovery_info is None:
return False
data = hass.data[DATA_MME].data
if not data.cars:
return False
MercedesMEDeviceTracker(hass, config, see, data)
return True
class MercedesMEDeviceTracker(object):
"""A class representing a Mercedes ME device tracker."""
def __init__(self, hass, config, see, data):
"""Initialize the Mercedes ME device tracker."""
self.see = see
self.data = data
self.update_info()
track_time_interval(
hass, self.update_info, MIN_TIME_BETWEEN_SCANS)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def update_info(self, now=None):
"""Update the device info."""
for device in self.data.cars:
if not device['services'].get('VEHICLE_FINDER', False):
continue
location = self.data.get_location(device["vin"])
if location is None:
continue
dev_id = device["vin"]
name = device["license"]
lat = location['positionLat']['value']
lon = location['positionLong']['value']
attrs = {
'trackr_id': dev_id,
'id': dev_id,
'name': name
}
self.see(
dev_id=dev_id, host_name=name,
gps=(lat, lon), attributes=attrs
)
return True

View File

@ -6,15 +6,15 @@ https://home-assistant.io/components/device_tracker.mysensors/
"""
from homeassistant.components import mysensors
from homeassistant.components.device_tracker import DOMAIN
from homeassistant.helpers.dispatcher import dispatcher_connect
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify
def setup_scanner(hass, config, see, discovery_info=None):
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up the MySensors device scanner."""
new_devices = mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsDeviceScanner,
device_args=(see, ))
device_args=(async_see, ))
if not new_devices:
return False
@ -22,9 +22,9 @@ def setup_scanner(hass, config, see, discovery_info=None):
dev_id = (
id(device.gateway), device.node_id, device.child_id,
device.value_type)
dispatcher_connect(
async_dispatcher_connect(
hass, mysensors.SIGNAL_CALLBACK.format(*dev_id),
device.update_callback)
device.async_update_callback)
return True
@ -32,20 +32,20 @@ def setup_scanner(hass, config, see, discovery_info=None):
class MySensorsDeviceScanner(mysensors.MySensorsDevice):
"""Represent a MySensors scanner."""
def __init__(self, see, *args):
def __init__(self, async_see, *args):
"""Set up instance."""
super().__init__(*args)
self.see = see
self.async_see = async_see
def update_callback(self):
async def async_update_callback(self):
"""Update the device."""
self.update()
await self.async_update()
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
position = child.values[self.value_type]
latitude, longitude, _ = position.split(',')
self.see(
await self.async_see(
dev_id=slugify(self.name),
host_name=self.name,
gps=(latitude, longitude),

View File

@ -95,7 +95,7 @@ class UbusDeviceScanner(DeviceScanner):
return self.last_results
def _generate_mac2name(self):
"""Return empty MAC to name dict. Overriden if DHCP server is set."""
"""Return empty MAC to name dict. Overridden if DHCP server is set."""
self.mac2name = dict()
@_refresh_on_access_denied

View File

@ -0,0 +1,77 @@
"""
Support for Xiaomi Mi WiFi Repeater 2.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/device_tracker.xiaomi_miio/
"""
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_TOKEN)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
})
REQUIREMENTS = ['python-miio==0.3.9']
def get_scanner(hass, config):
"""Return a Xiaomi MiIO device scanner."""
from miio import WifiRepeater, DeviceException
scanner = None
host = config[DOMAIN].get(CONF_HOST)
token = config[DOMAIN].get(CONF_TOKEN)
_LOGGER.info(
"Initializing with host %s (token %s...)", host, token[:5])
try:
device = WifiRepeater(host, token)
device_info = device.info()
_LOGGER.info("%s %s %s detected",
device_info.model,
device_info.firmware_version,
device_info.hardware_version)
scanner = XiaomiMiioDeviceScanner(hass, device)
except DeviceException as ex:
_LOGGER.error("Device unavailable or token incorrect: %s", ex)
return scanner
class XiaomiMiioDeviceScanner(DeviceScanner):
"""This class queries a Xiaomi Mi WiFi Repeater."""
def __init__(self, hass, device):
"""Initialize the scanner."""
self.device = device
async def async_scan_devices(self):
"""Scan for devices and return a list containing found device ids."""
from miio import DeviceException
devices = []
try:
station_info = await self.hass.async_add_job(self.device.status)
_LOGGER.debug("Got new station info: %s", station_info)
for device in station_info['mat']:
devices.append(device['mac'])
except DeviceException as ex:
_LOGGER.error("Got exception while fetching the state: %s", ex)
return devices
async def async_get_device_name(self, device):
"""The repeater doesn't provide the name of the associated device."""
return None

View File

@ -13,6 +13,7 @@ import os
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START
import homeassistant.helpers.config_validation as cv
@ -40,6 +41,10 @@ SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
CONFIG_ENTRY_HANDLERS = {
SERVICE_HUE: 'hue',
}
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
SERVICE_NETGEAR: ('device_tracker', None),
@ -51,7 +56,6 @@ SERVICE_HANDLERS = {
SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
SERVICE_HUE: ('hue', None),
SERVICE_DECONZ: ('deconz', None),
SERVICE_DAIKIN: ('daikin', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
@ -105,6 +109,20 @@ async def async_setup(hass, config):
logger.info("Ignoring service: %s %s", service, info)
return
discovery_hash = json.dumps([service, info], sort_keys=True)
if discovery_hash in already_discovered:
return
already_discovered.add(discovery_hash)
if service in CONFIG_ENTRY_HANDLERS:
await hass.config_entries.flow.async_init(
CONFIG_ENTRY_HANDLERS[service],
source=config_entries.SOURCE_DISCOVERY,
data=info
)
return
comp_plat = SERVICE_HANDLERS.get(service)
# We do not know how to handle this service.
@ -112,12 +130,6 @@ async def async_setup(hass, config):
logger.info("Unknown service discovered: %s %s", service, info)
return
discovery_hash = json.dumps([service, info], sort_keys=True)
if discovery_hash in already_discovered:
return
already_discovered.add(discovery_hash)
logger.info("Found new service: %s %s", service, info)
component, platform = comp_plat

View File

@ -22,6 +22,7 @@ DOMAIN = 'doorbird'
API_URL = '/api/{}'.format(DOMAIN)
CONF_DOORBELL_EVENTS = 'doorbell_events'
CONF_CUSTOM_URL = 'hass_url_override'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@ -29,6 +30,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
vol.Optional(CONF_CUSTOM_URL): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
@ -61,9 +63,17 @@ def setup(hass, config):
# Provide an endpoint for the device to call to trigger events
hass.http.register_view(DoorbirdRequestView())
# Get the URL of this server
hass_url = hass.config.api.base_url
# Override it if another is specified in the component configuration
if config[DOMAIN].get(CONF_CUSTOM_URL):
hass_url = config[DOMAIN].get(CONF_CUSTOM_URL)
_LOGGER.info("DoorBird will connect to this instance via %s",
hass_url)
# This will make HA the only service that gets doorbell events
url = '{}{}/{}'.format(
hass.config.api.base_url, API_URL, SENSOR_DOORBELL)
url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL)
device.reset_notifications()
device.subscribe_notification(SENSOR_DOORBELL, url)

View File

@ -158,10 +158,6 @@ class Config(object):
"Listen port not specified, defaulting to %s",
self.listen_port)
if self.type == TYPE_GOOGLE and self.listen_port != 80:
_LOGGER.warning("When targeting Google Home, listening port has "
"to be port 80")
# Get whether or not UPNP binds to multicast address (239.255.255.250)
# or to the unicast address (host_ip_addr)
self.upnp_bind_multicast = conf.get(

View File

@ -68,50 +68,50 @@ xiaomi_miio_set_buzzer_on:
description: Turn the buzzer on.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_buzzer_off:
description: Turn the buzzer off.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_led_on:
description: Turn the led on.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_led_off:
description: Turn the led off.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_child_lock_on:
description: Turn the child lock on.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_child_lock_off:
description: Turn the child lock off.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_favorite_level:
description: Set the favorite level.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
level:
description: Level, between 0 and 16.
example: 1
@ -120,8 +120,87 @@ xiaomi_miio_set_led_brightness:
description: Set the led brightness.
fields:
entity_id:
description: Name of the air purifier entity.
example: 'fan.xiaomi_air_purifier'
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
brightness:
description: Brightness (0 = Bright, 1 = Dim, 2 = Off)
example: 1
xiaomi_miio_set_auto_detect_on:
description: Turn the auto detect on.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_auto_detect_off:
description: Turn the auto detect off.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_learn_mode_on:
description: Turn the learn mode on.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_learn_mode_off:
description: Turn the learn mode off.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_volume:
description: Set the sound volume.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
volume:
description: Volume, between 0 and 100.
example: 50
xiaomi_miio_reset_filter:
description: Reset the filter lifetime and usage.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_extra_features:
description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
features:
description: Integer, known values are 0 (default) and 1 (turbo mode).
example: 1
xiaomi_miio_set_target_humidity:
description: Set the target humidity.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
humidity:
description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80.
example: 50
xiaomi_miio_set_dry_on:
description: Turn the dry mode on.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_dry_off:
description: Turn the dry mode off.
fields:
entity_id:
description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_miio_device'

View File

@ -1,16 +1,16 @@
"""
Support for Xiaomi Mi Air Purifier 2.
Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/fan.xiaomi_miio/
"""
import asyncio
from enum import Enum
from functools import partial
import logging
import voluptuous as vol
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
SUPPORT_SET_SPEED, DOMAIN, )
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
@ -20,17 +20,40 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Air Purifier'
PLATFORM = 'xiaomi_miio'
DEFAULT_NAME = 'Xiaomi Miio Device'
DATA_KEY = 'fan.xiaomi_miio'
CONF_MODEL = 'model'
MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6'
MODEL_AIRPURIFIER_V3 = 'zhimi.airpurifier.v3'
MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1'
MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_MODEL): vol.In(
['zhimi.airpurifier.m1',
'zhimi.airpurifier.m2',
'zhimi.airpurifier.ma1',
'zhimi.airpurifier.ma2',
'zhimi.airpurifier.sa1',
'zhimi.airpurifier.sa2',
'zhimi.airpurifier.v1',
'zhimi.airpurifier.v2',
'zhimi.airpurifier.v3',
'zhimi.airpurifier.v5',
'zhimi.airpurifier.v6',
'zhimi.humidifier.v1',
'zhimi.humidifier.ca1']),
})
REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
ATTR_MODEL = 'model'
# Air Purifier
ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity'
ATTR_AIR_QUALITY_INDEX = 'aqi'
@ -45,20 +68,190 @@ ATTR_LED_BRIGHTNESS = 'led_brightness'
ATTR_MOTOR_SPEED = 'motor_speed'
ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi'
ATTR_PURIFY_VOLUME = 'purify_volume'
ATTR_BRIGHTNESS = 'brightness'
ATTR_LEVEL = 'level'
ATTR_MOTOR2_SPEED = 'motor2_speed'
ATTR_ILLUMINANCE = 'illuminance'
ATTR_FILTER_RFID_PRODUCT_ID = 'filter_rfid_product_id'
ATTR_FILTER_RFID_TAG = 'filter_rfid_tag'
ATTR_FILTER_TYPE = 'filter_type'
ATTR_LEARN_MODE = 'learn_mode'
ATTR_SLEEP_TIME = 'sleep_time'
ATTR_SLEEP_LEARN_COUNT = 'sleep_mode_learn_count'
ATTR_EXTRA_FEATURES = 'extra_features'
ATTR_FEATURES = 'features'
ATTR_TURBO_MODE_SUPPORTED = 'turbo_mode_supported'
ATTR_AUTO_DETECT = 'auto_detect'
ATTR_SLEEP_MODE = 'sleep_mode'
ATTR_VOLUME = 'volume'
ATTR_USE_TIME = 'use_time'
ATTR_BUTTON_PRESSED = 'button_pressed'
# Air Humidifier
ATTR_TARGET_HUMIDITY = 'target_humidity'
ATTR_TRANS_LEVEL = 'trans_level'
ATTR_HARDWARE_VERSION = 'hardware_version'
# Air Humidifier CA
ATTR_SPEED = 'speed'
ATTR_DEPTH = 'depth'
ATTR_DRY = 'dry'
# Map attributes to properties of the state object
AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = {
ATTR_TEMPERATURE: 'temperature',
ATTR_HUMIDITY: 'humidity',
ATTR_AIR_QUALITY_INDEX: 'aqi',
ATTR_MODE: 'mode',
ATTR_FILTER_HOURS_USED: 'filter_hours_used',
ATTR_FILTER_LIFE: 'filter_life_remaining',
ATTR_FAVORITE_LEVEL: 'favorite_level',
ATTR_CHILD_LOCK: 'child_lock',
ATTR_LED: 'led',
ATTR_MOTOR_SPEED: 'motor_speed',
ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
ATTR_PURIFY_VOLUME: 'purify_volume',
ATTR_LEARN_MODE: 'learn_mode',
ATTR_SLEEP_TIME: 'sleep_time',
ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
ATTR_EXTRA_FEATURES: 'extra_features',
ATTR_TURBO_MODE_SUPPORTED: 'turbo_mode_supported',
ATTR_AUTO_DETECT: 'auto_detect',
ATTR_USE_TIME: 'use_time',
ATTR_BUTTON_PRESSED: 'button_pressed',
}
AVAILABLE_ATTRIBUTES_AIRPURIFIER = {
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
ATTR_BUZZER: 'buzzer',
ATTR_LED_BRIGHTNESS: 'led_brightness',
ATTR_SLEEP_MODE: 'sleep_mode',
}
AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = {
**AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON,
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
ATTR_FILTER_TYPE: 'filter_type',
ATTR_ILLUMINANCE: 'illuminance',
ATTR_MOTOR2_SPEED: 'motor2_speed',
ATTR_VOLUME: 'volume',
}
AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = {
# Common set isn't used here. It's a very basic version of the device.
ATTR_AIR_QUALITY_INDEX: 'aqi',
ATTR_MODE: 'mode',
ATTR_LED: 'led',
ATTR_BUZZER: 'buzzer',
ATTR_CHILD_LOCK: 'child_lock',
ATTR_ILLUMINANCE: 'illuminance',
ATTR_FILTER_HOURS_USED: 'filter_hours_used',
ATTR_FILTER_LIFE: 'filter_life_remaining',
ATTR_MOTOR_SPEED: 'motor_speed',
# perhaps supported but unconfirmed
ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi',
ATTR_VOLUME: 'volume',
ATTR_MOTOR2_SPEED: 'motor2_speed',
ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id',
ATTR_FILTER_RFID_TAG: 'filter_rfid_tag',
ATTR_FILTER_TYPE: 'filter_type',
ATTR_PURIFY_VOLUME: 'purify_volume',
ATTR_LEARN_MODE: 'learn_mode',
ATTR_SLEEP_TIME: 'sleep_time',
ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count',
ATTR_EXTRA_FEATURES: 'extra_features',
ATTR_AUTO_DETECT: 'auto_detect',
ATTR_USE_TIME: 'use_time',
ATTR_BUTTON_PRESSED: 'button_pressed',
}
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = {
ATTR_TEMPERATURE: 'temperature',
ATTR_HUMIDITY: 'humidity',
ATTR_MODE: 'mode',
ATTR_BUZZER: 'buzzer',
ATTR_CHILD_LOCK: 'child_lock',
ATTR_TRANS_LEVEL: 'trans_level',
ATTR_TARGET_HUMIDITY: 'target_humidity',
ATTR_LED_BRIGHTNESS: 'led_brightness',
ATTR_BUTTON_PRESSED: 'button_pressed',
ATTR_USE_TIME: 'use_time',
ATTR_HARDWARE_VERSION: 'hardware_version',
}
AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = {
**AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER,
ATTR_SPEED: 'speed',
ATTR_DEPTH: 'depth',
ATTR_DRY: 'dry',
}
OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle']
OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite']
OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle',
'Medium', 'High', 'Strong']
SUCCESS = ['ok']
FEATURE_SET_BUZZER = 1
FEATURE_SET_LED = 2
FEATURE_SET_CHILD_LOCK = 4
FEATURE_SET_LED_BRIGHTNESS = 8
FEATURE_SET_FAVORITE_LEVEL = 16
FEATURE_SET_AUTO_DETECT = 32
FEATURE_SET_LEARN_MODE = 64
FEATURE_SET_VOLUME = 128
FEATURE_RESET_FILTER = 256
FEATURE_SET_EXTRA_FEATURES = 512
FEATURE_SET_TARGET_HUMIDITY = 1024
FEATURE_SET_DRY = 2048
FEATURE_FLAGS_GENERIC = (FEATURE_SET_BUZZER |
FEATURE_SET_CHILD_LOCK)
FEATURE_FLAGS_AIRPURIFIER = (FEATURE_FLAGS_GENERIC |
FEATURE_SET_LED |
FEATURE_SET_LED_BRIGHTNESS |
FEATURE_SET_FAVORITE_LEVEL |
FEATURE_SET_LEARN_MODE |
FEATURE_RESET_FILTER |
FEATURE_SET_EXTRA_FEATURES)
FEATURE_FLAGS_AIRPURIFIER_PRO = (FEATURE_SET_CHILD_LOCK |
FEATURE_SET_LED |
FEATURE_SET_FAVORITE_LEVEL |
FEATURE_SET_AUTO_DETECT |
FEATURE_SET_VOLUME)
FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_FLAGS_GENERIC |
FEATURE_SET_LED)
FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_FLAGS_GENERIC |
FEATURE_SET_LED_BRIGHTNESS |
FEATURE_SET_TARGET_HUMIDITY)
FEATURE_FLAGS_AIRHUMIDIFIER_CA = (FEATURE_FLAGS_AIRHUMIDIFIER |
FEATURE_SET_DRY)
SERVICE_SET_BUZZER_ON = 'xiaomi_miio_set_buzzer_on'
SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off'
SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on'
SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off'
SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on'
SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off'
SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level'
SERVICE_SET_LED_BRIGHTNESS = 'xiaomi_miio_set_led_brightness'
SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level'
SERVICE_SET_AUTO_DETECT_ON = 'xiaomi_miio_set_auto_detect_on'
SERVICE_SET_AUTO_DETECT_OFF = 'xiaomi_miio_set_auto_detect_off'
SERVICE_SET_LEARN_MODE_ON = 'xiaomi_miio_set_learn_mode_on'
SERVICE_SET_LEARN_MODE_OFF = 'xiaomi_miio_set_learn_mode_off'
SERVICE_SET_VOLUME = 'xiaomi_miio_set_volume'
SERVICE_RESET_FILTER = 'xiaomi_miio_reset_filter'
SERVICE_SET_EXTRA_FEATURES = 'xiaomi_miio_set_extra_features'
SERVICE_SET_TARGET_HUMIDITY = 'xiaomi_miio_set_target_humidity'
SERVICE_SET_DRY_ON = 'xiaomi_miio_set_dry_on'
SERVICE_SET_DRY_OFF = 'xiaomi_miio_set_dry_off'
AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
@ -74,6 +267,21 @@ SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend({
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=16))
})
SERVICE_SCHEMA_VOLUME = AIRPURIFIER_SERVICE_SCHEMA.extend({
vol.Required(ATTR_VOLUME):
vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100))
})
SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend({
vol.Required(ATTR_FEATURES):
vol.All(vol.Coerce(int), vol.Range(min=0))
})
SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend({
vol.Required(ATTR_HUMIDITY):
vol.All(vol.Coerce(int), vol.In([30, 40, 50, 60, 70, 80]))
})
SERVICE_TO_METHOD = {
SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'},
SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'},
@ -81,59 +289,99 @@ SERVICE_TO_METHOD = {
SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'},
SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'},
SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'},
SERVICE_SET_FAVORITE_LEVEL: {
'method': 'async_set_favorite_level',
'schema': SERVICE_SCHEMA_FAVORITE_LEVEL},
SERVICE_SET_AUTO_DETECT_ON: {'method': 'async_set_auto_detect_on'},
SERVICE_SET_AUTO_DETECT_OFF: {'method': 'async_set_auto_detect_off'},
SERVICE_SET_LEARN_MODE_ON: {'method': 'async_set_learn_mode_on'},
SERVICE_SET_LEARN_MODE_OFF: {'method': 'async_set_learn_mode_off'},
SERVICE_RESET_FILTER: {'method': 'async_reset_filter'},
SERVICE_SET_LED_BRIGHTNESS: {
'method': 'async_set_led_brightness',
'schema': SERVICE_SCHEMA_LED_BRIGHTNESS},
SERVICE_SET_FAVORITE_LEVEL: {
'method': 'async_set_favorite_level',
'schema': SERVICE_SCHEMA_FAVORITE_LEVEL},
SERVICE_SET_VOLUME: {
'method': 'async_set_volume',
'schema': SERVICE_SCHEMA_VOLUME},
SERVICE_SET_EXTRA_FEATURES: {
'method': 'async_set_extra_features',
'schema': SERVICE_SCHEMA_EXTRA_FEATURES},
SERVICE_SET_TARGET_HUMIDITY: {
'method': 'async_set_target_humidity',
'schema': SERVICE_SCHEMA_TARGET_HUMIDITY},
SERVICE_SET_DRY_ON: {'method': 'async_set_dry_on'},
SERVICE_SET_DRY_OFF: {'method': 'async_set_dry_off'},
}
# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the air purifier from config."""
from miio import AirPurifier, DeviceException
if PLATFORM not in hass.data:
hass.data[PLATFORM] = {}
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the miio fan device from config."""
from miio import Device, DeviceException
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
token = config.get(CONF_TOKEN)
model = config.get(CONF_MODEL)
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
unique_id = None
try:
if model is None:
try:
miio_device = Device(host, token)
device_info = miio_device.info()
model = device_info.model
unique_id = "{}-{}".format(model, device_info.mac_address)
_LOGGER.info("%s %s %s detected",
model,
device_info.firmware_version,
device_info.hardware_version)
except DeviceException:
raise PlatformNotReady
if model.startswith('zhimi.airpurifier.'):
from miio import AirPurifier
air_purifier = AirPurifier(host, token)
device = XiaomiAirPurifier(name, air_purifier, model, unique_id)
elif model.startswith('zhimi.humidifier.'):
from miio import AirHumidifier
air_humidifier = AirHumidifier(host, token)
device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id)
else:
_LOGGER.error(
'Unsupported device found! Please create an issue at '
'https://github.com/syssi/xiaomi_airpurifier/issues '
'and provide the following data: %s', model)
return False
xiaomi_air_purifier = XiaomiAirPurifier(name, air_purifier)
hass.data[PLATFORM][host] = xiaomi_air_purifier
except DeviceException:
raise PlatformNotReady
hass.data[DATA_KEY][host] = device
async_add_devices([device], update_before_add=True)
async_add_devices([xiaomi_air_purifier], update_before_add=True)
@asyncio.coroutine
def async_service_handler(service):
async def async_service_handler(service):
"""Map services to methods on XiaomiAirPurifier."""
method = SERVICE_TO_METHOD.get(service.service)
params = {key: value for key, value in service.data.items()
if key != ATTR_ENTITY_ID}
entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids:
devices = [device for device in hass.data[PLATFORM].values() if
devices = [device for device in hass.data[DATA_KEY].values() if
device.entity_id in entity_ids]
else:
devices = hass.data[PLATFORM].values()
devices = hass.data[DATA_KEY].values()
update_tasks = []
for device in devices:
yield from getattr(device, method['method'])(**params)
if not hasattr(device, method['method']):
continue
await getattr(device, method['method'])(**params)
update_tasks.append(device.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
await asyncio.wait(update_tasks, loop=hass.loop)
for air_purifier_service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[air_purifier_service].get(
@ -142,31 +390,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
DOMAIN, air_purifier_service, async_service_handler, schema=schema)
class XiaomiAirPurifier(FanEntity):
"""Representation of a Xiaomi Air Purifier."""
class XiaomiGenericDevice(FanEntity):
"""Representation of a generic Xiaomi device."""
def __init__(self, name, air_purifier):
"""Initialize the air purifier."""
def __init__(self, name, device, model, unique_id):
"""Initialize the generic Xiaomi device."""
self._name = name
self._device = device
self._model = model
self._unique_id = unique_id
self._air_purifier = air_purifier
self._available = False
self._state = None
self._state_attrs = {
ATTR_AIR_QUALITY_INDEX: None,
ATTR_TEMPERATURE: None,
ATTR_HUMIDITY: None,
ATTR_MODE: None,
ATTR_FILTER_HOURS_USED: None,
ATTR_FILTER_LIFE: None,
ATTR_FAVORITE_LEVEL: None,
ATTR_BUZZER: None,
ATTR_CHILD_LOCK: None,
ATTR_LED: None,
ATTR_LED_BRIGHTNESS: None,
ATTR_MOTOR_SPEED: None,
ATTR_AVERAGE_AIR_QUALITY_INDEX: None,
ATTR_PURIFY_VOLUME: None,
ATTR_MODEL: self._model,
}
self._device_features = FEATURE_FLAGS_GENERIC
self._skip_update = False
@property
@ -176,9 +415,14 @@ class XiaomiAirPurifier(FanEntity):
@property
def should_poll(self):
"""Poll the fan."""
"""Poll the device."""
return True
@property
def unique_id(self):
"""Return an unique ID."""
return self._unique_id
@property
def name(self):
"""Return the name of the device if any."""
@ -187,7 +431,7 @@ class XiaomiAirPurifier(FanEntity):
@property
def available(self):
"""Return true when state is known."""
return self._state is not None
return self._available
@property
def device_state_attributes(self):
@ -196,50 +440,116 @@ class XiaomiAirPurifier(FanEntity):
@property
def is_on(self):
"""Return true if fan is on."""
"""Return true if device is on."""
return self._state
@asyncio.coroutine
def _try_command(self, mask_error, func, *args, **kwargs):
"""Call an air purifier command handling error messages."""
@staticmethod
def _extract_value_from_attribute(state, attribute):
value = getattr(state, attribute)
if isinstance(value, Enum):
return value.value
return value
async def _try_command(self, mask_error, func, *args, **kwargs):
"""Call a miio device command handling error messages."""
from miio import DeviceException
try:
result = yield from self.hass.async_add_job(
result = await self.hass.async_add_job(
partial(func, *args, **kwargs))
_LOGGER.debug("Response received from air purifier: %s", result)
_LOGGER.debug("Response received from miio device: %s", result)
return result == SUCCESS
except DeviceException as exc:
_LOGGER.error(mask_error, exc)
self._available = False
return False
@asyncio.coroutine
def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None:
"""Turn the fan on."""
async def async_turn_on(self, speed: str = None,
**kwargs) -> None:
"""Turn the device on."""
if speed:
# If operation mode was set the device must not be turned on.
result = yield from self.async_set_speed(speed)
result = await self.async_set_speed(speed)
else:
result = yield from self._try_command(
"Turning the air purifier on failed.", self._air_purifier.on)
result = await self._try_command(
"Turning the miio device on failed.", self._device.on)
if result:
self._state = True
self._skip_update = True
@asyncio.coroutine
def async_turn_off(self: ToggleEntity, **kwargs) -> None:
"""Turn the fan off."""
result = yield from self._try_command(
"Turning the air purifier off failed.", self._air_purifier.off)
async def async_turn_off(self, **kwargs) -> None:
"""Turn the device off."""
result = await self._try_command(
"Turning the miio device off failed.", self._device.off)
if result:
self._state = False
self._skip_update = True
@asyncio.coroutine
def async_update(self):
async def async_set_buzzer_on(self):
"""Turn the buzzer on."""
if self._device_features & FEATURE_SET_BUZZER == 0:
return
await self._try_command(
"Turning the buzzer of the miio device on failed.",
self._device.set_buzzer, True)
async def async_set_buzzer_off(self):
"""Turn the buzzer off."""
if self._device_features & FEATURE_SET_BUZZER == 0:
return
await self._try_command(
"Turning the buzzer of the miio device off failed.",
self._device.set_buzzer, False)
async def async_set_child_lock_on(self):
"""Turn the child lock on."""
if self._device_features & FEATURE_SET_CHILD_LOCK == 0:
return
await self._try_command(
"Turning the child lock of the miio device on failed.",
self._device.set_child_lock, True)
async def async_set_child_lock_off(self):
"""Turn the child lock off."""
if self._device_features & FEATURE_SET_CHILD_LOCK == 0:
return
await self._try_command(
"Turning the child lock of the miio device off failed.",
self._device.set_child_lock, False)
class XiaomiAirPurifier(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Purifier."""
def __init__(self, name, device, model, unique_id):
"""Initialize the plug switch."""
super().__init__(name, device, model, unique_id)
if self._model == MODEL_AIRPURIFIER_PRO:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO
self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO
elif self._model == MODEL_AIRPURIFIER_V3:
self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3
self._speed_list = OPERATION_MODES_AIRPURIFIER_V3
else:
self._device_features = FEATURE_FLAGS_AIRPURIFIER
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER
self._speed_list = OPERATION_MODES_AIRPURIFIER
self._state_attrs.update(
{attribute: None for attribute in self._available_attributes})
async def async_update(self):
"""Fetch state from the device."""
from miio import DeviceException
@ -249,40 +559,24 @@ class XiaomiAirPurifier(FanEntity):
return
try:
state = yield from self.hass.async_add_job(
self._air_purifier.status)
state = await self.hass.async_add_job(
self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on
self._state_attrs = {
ATTR_TEMPERATURE: state.temperature,
ATTR_HUMIDITY: state.humidity,
ATTR_AIR_QUALITY_INDEX: state.aqi,
ATTR_MODE: state.mode.value,
ATTR_FILTER_HOURS_USED: state.filter_hours_used,
ATTR_FILTER_LIFE: state.filter_life_remaining,
ATTR_FAVORITE_LEVEL: state.favorite_level,
ATTR_BUZZER: state.buzzer,
ATTR_CHILD_LOCK: state.child_lock,
ATTR_LED: state.led,
ATTR_MOTOR_SPEED: state.motor_speed,
ATTR_AVERAGE_AIR_QUALITY_INDEX: state.average_aqi,
ATTR_PURIFY_VOLUME: state.purify_volume,
}
if state.led_brightness:
self._state_attrs[
ATTR_LED_BRIGHTNESS] = state.led_brightness.value
self._state_attrs.update(
{key: self._extract_value_from_attribute(state, value) for
key, value in self._available_attributes.items()})
except DeviceException as ex:
self._state = None
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
def speed_list(self: ToggleEntity) -> list:
def speed_list(self) -> list:
"""Get the list of available speeds."""
from miio.airpurifier import OperationMode
return [mode.name for mode in OperationMode]
return self._speed_list
@property
def speed(self):
@ -294,70 +588,227 @@ class XiaomiAirPurifier(FanEntity):
return None
@asyncio.coroutine
def async_set_speed(self: ToggleEntity, speed: str) -> None:
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
_LOGGER.debug("Setting the operation mode to: %s", speed)
if self.supported_features & SUPPORT_SET_SPEED == 0:
return
from miio.airpurifier import OperationMode
yield from self._try_command(
"Setting operation mode of the air purifier failed.",
self._air_purifier.set_mode, OperationMode[speed.title()])
_LOGGER.debug("Setting the operation mode to: %s", speed)
@asyncio.coroutine
def async_set_buzzer_on(self):
"""Turn the buzzer on."""
yield from self._try_command(
"Turning the buzzer of the air purifier on failed.",
self._air_purifier.set_buzzer, True)
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode, OperationMode[speed.title()])
@asyncio.coroutine
def async_set_buzzer_off(self):
"""Turn the buzzer off."""
yield from self._try_command(
"Turning the buzzer of the air purifier off failed.",
self._air_purifier.set_buzzer, False)
@asyncio.coroutine
def async_set_led_on(self):
async def async_set_led_on(self):
"""Turn the led on."""
yield from self._try_command(
"Turning the led of the air purifier off failed.",
self._air_purifier.set_led, True)
if self._device_features & FEATURE_SET_LED == 0:
return
@asyncio.coroutine
def async_set_led_off(self):
await self._try_command(
"Turning the led of the miio device off failed.",
self._device.set_led, True)
async def async_set_led_off(self):
"""Turn the led off."""
yield from self._try_command(
"Turning the led of the air purifier off failed.",
self._air_purifier.set_led, False)
if self._device_features & FEATURE_SET_LED == 0:
return
@asyncio.coroutine
def async_set_child_lock_on(self):
"""Turn the child lock on."""
yield from self._try_command(
"Turning the child lock of the air purifier on failed.",
self._air_purifier.set_child_lock, True)
await self._try_command(
"Turning the led of the miio device off failed.",
self._device.set_led, False)
@asyncio.coroutine
def async_set_child_lock_off(self):
"""Turn the child lock off."""
yield from self._try_command(
"Turning the child lock of the air purifier off failed.",
self._air_purifier.set_child_lock, False)
@asyncio.coroutine
def async_set_led_brightness(self, brightness: int = 2):
async def async_set_led_brightness(self, brightness: int = 2):
"""Set the led brightness."""
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
return
from miio.airpurifier import LedBrightness
yield from self._try_command(
"Setting the led brightness of the air purifier failed.",
self._air_purifier.set_led_brightness, LedBrightness(brightness))
await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness, LedBrightness(brightness))
@asyncio.coroutine
def async_set_favorite_level(self, level: int = 1):
async def async_set_favorite_level(self, level: int = 1):
"""Set the favorite level."""
yield from self._try_command(
"Setting the favorite level of the air purifier failed.",
self._air_purifier.set_favorite_level, level)
if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0:
return
await self._try_command(
"Setting the favorite level of the miio device failed.",
self._device.set_favorite_level, level)
async def async_set_auto_detect_on(self):
"""Turn the auto detect on."""
if self._device_features & FEATURE_SET_AUTO_DETECT == 0:
return
await self._try_command(
"Turning the auto detect of the miio device on failed.",
self._device.set_auto_detect, True)
async def async_set_auto_detect_off(self):
"""Turn the auto detect off."""
if self._device_features & FEATURE_SET_AUTO_DETECT == 0:
return
await self._try_command(
"Turning the auto detect of the miio device off failed.",
self._device.set_auto_detect, False)
async def async_set_learn_mode_on(self):
"""Turn the learn mode on."""
if self._device_features & FEATURE_SET_LEARN_MODE == 0:
return
await self._try_command(
"Turning the learn mode of the miio device on failed.",
self._device.set_learn_mode, True)
async def async_set_learn_mode_off(self):
"""Turn the learn mode off."""
if self._device_features & FEATURE_SET_LEARN_MODE == 0:
return
await self._try_command(
"Turning the learn mode of the miio device off failed.",
self._device.set_learn_mode, False)
async def async_set_volume(self, volume: int = 50):
"""Set the sound volume."""
if self._device_features & FEATURE_SET_VOLUME == 0:
return
await self._try_command(
"Setting the sound volume of the miio device failed.",
self._device.set_volume, volume)
async def async_set_extra_features(self, features: int = 1):
"""Set the extra features."""
if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0:
return
await self._try_command(
"Setting the extra features of the miio device failed.",
self._device.set_extra_features, features)
async def async_reset_filter(self):
"""Reset the filter lifetime and usage."""
if self._device_features & FEATURE_RESET_FILTER == 0:
return
await self._try_command(
"Resetting the filter lifetime of the miio device failed.",
self._device.reset_filter)
class XiaomiAirHumidifier(XiaomiGenericDevice):
"""Representation of a Xiaomi Air Humidifier."""
def __init__(self, name, device, model, unique_id):
"""Initialize the plug switch."""
from miio.airpurifier import OperationMode
super().__init__(name, device, model, unique_id)
if self._model == MODEL_AIRHUMIDIFIER_CA:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA
self._speed_list = [mode.name for mode in OperationMode]
else:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER
self._speed_list = [mode.name for mode in OperationMode if
mode.name != 'Auto']
self._state_attrs.update(
{attribute: None for attribute in self._available_attributes})
async def async_update(self):
"""Fetch state from the device."""
from miio import DeviceException
# On state change the device doesn't provide the new state immediately.
if self._skip_update:
self._skip_update = False
return
try:
state = await self.hass.async_add_job(self._device.status)
_LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on
self._state_attrs.update(
{key: self._extract_value_from_attribute(state, value) for
key, value in self._available_attributes.items()})
except DeviceException as ex:
self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex)
def speed_list(self) -> list:
"""Get the list of available speeds."""
return self._speed_list
@property
def speed(self):
"""Return the current speed."""
if self._state:
from miio.airhumidifier import OperationMode
return OperationMode(self._state_attrs[ATTR_MODE]).name
return None
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
if self.supported_features & SUPPORT_SET_SPEED == 0:
return
from miio.airhumidifier import OperationMode
_LOGGER.debug("Setting the operation mode to: %s", speed)
await self._try_command(
"Setting operation mode of the miio device failed.",
self._device.set_mode, OperationMode[speed.title()])
async def async_set_led_brightness(self, brightness: int = 2):
"""Set the led brightness."""
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
return
from miio.airhumidifier import LedBrightness
await self._try_command(
"Setting the led brightness of the miio device failed.",
self._device.set_led_brightness, LedBrightness(brightness))
async def async_set_target_humidity(self, humidity: int = 40):
"""Set the target humidity."""
if self._device_features & FEATURE_SET_TARGET_HUMIDITY == 0:
return
await self._try_command(
"Setting the target humidity of the miio device failed.",
self._device.set_target_humidity, humidity)
async def async_set_dry_on(self):
"""Turn the dry mode on."""
if self._device_features & FEATURE_SET_DRY == 0:
return
await self._try_command(
"Turning the dry mode of the miio device off failed.",
self._device.set_dry, True)
async def async_set_dry_off(self):
"""Turn the dry mode off."""
if self._device_features & FEATURE_SET_DRY == 0:
return
await self._try_command(
"Turning the dry mode of the miio device off failed.",
self._device.set_dry, False)

View File

@ -0,0 +1,110 @@
"""
Component for monitoring activity on a folder.
For more details about this platform, refer to the documentation at
https://home-assistant.io/components/folder_watcher/
"""
import os
import logging
import voluptuous as vol
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['watchdog==0.8.3']
_LOGGER = logging.getLogger(__name__)
CONF_FOLDER = 'folder'
CONF_PATTERNS = 'patterns'
DEFAULT_PATTERN = '*'
DOMAIN = "folder_watcher"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_FOLDER): cv.isdir,
vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]):
vol.All(cv.ensure_list, [cv.string]),
})])
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the folder watcher."""
conf = config[DOMAIN]
for watcher in conf:
path = watcher[CONF_FOLDER]
patterns = watcher[CONF_PATTERNS]
if not hass.config.is_allowed_path(path):
_LOGGER.error("folder %s is not valid or allowed", path)
return False
Watcher(path, patterns, hass)
return True
def create_event_handler(patterns, hass):
""""Return the Watchdog EventHandler object."""
from watchdog.events import PatternMatchingEventHandler
class EventHandler(PatternMatchingEventHandler):
"""Class for handling Watcher events."""
def __init__(self, patterns, hass):
"""Initialise the EventHandler."""
super().__init__(patterns)
self.hass = hass
def process(self, event):
"""On Watcher event, fire HA event."""
_LOGGER.debug("process(%s)", event)
if not event.is_directory:
folder, file_name = os.path.split(event.src_path)
self.hass.bus.fire(
DOMAIN, {
"event_type": event.event_type,
'path': event.src_path,
'file': file_name,
'folder': folder,
})
def on_modified(self, event):
"""File modified."""
self.process(event)
def on_moved(self, event):
"""File moved."""
self.process(event)
def on_created(self, event):
"""File created."""
self.process(event)
def on_deleted(self, event):
"""File deleted."""
self.process(event)
return EventHandler(patterns, hass)
class Watcher():
"""Class for starting Watchdog."""
def __init__(self, path, patterns, hass):
"""Initialise the watchdog observer."""
from watchdog.observers import Observer
self._observer = Observer()
self._observer.schedule(
create_event_handler(patterns, hass),
path,
recursive=True)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.startup)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
def startup(self, event):
"""Start the watcher."""
self._observer.start()
def shutdown(self, event):
"""Shutdown the watcher."""
self._observer.stop()
self._observer.join()

View File

@ -0,0 +1,103 @@
"""
Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/freedns/
"""
import asyncio
from datetime import timedelta
import logging
import aiohttp
import async_timeout
import voluptuous as vol
from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'freedns'
DEFAULT_INTERVAL = timedelta(minutes=10)
TIMEOUT = 10
UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php'
CONF_UPDATE_INTERVAL = 'update_interval'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Exclusive(CONF_URL, DOMAIN): cv.string,
vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string,
vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): vol.All(
cv.time_period, cv.positive_timedelta),
})
}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass, config):
"""Initialize the FreeDNS component."""
url = config[DOMAIN].get(CONF_URL)
auth_token = config[DOMAIN].get(CONF_ACCESS_TOKEN)
update_interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL)
session = hass.helpers.aiohttp_client.async_get_clientsession()
result = yield from _update_freedns(
hass, session, url, auth_token)
if result is False:
return False
@asyncio.coroutine
def update_domain_callback(now):
"""Update the FreeDNS entry."""
yield from _update_freedns(hass, session, url, auth_token)
hass.helpers.event.async_track_time_interval(
update_domain_callback, update_interval)
return True
@asyncio.coroutine
def _update_freedns(hass, session, url, auth_token):
"""Update FreeDNS."""
params = None
if url is None:
url = UPDATE_URL
if auth_token is not None:
params = {}
params[auth_token] = ""
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
resp = yield from session.get(url, params=params)
body = yield from resp.text()
if "has not changed" in body:
# IP has not changed.
_LOGGER.debug("FreeDNS update skipped: IP has not changed")
return True
if "ERROR" not in body:
_LOGGER.debug("Updating FreeDNS was successful: %s", body)
return True
if "Invalid update URL" in body:
_LOGGER.error("FreeDNS update token is invalid")
else:
_LOGGER.warning("Updating FreeDNS failed: %s", body)
except aiohttp.ClientError:
_LOGGER.warning("Can't connect to FreeDNS API")
except asyncio.TimeoutError:
_LOGGER.warning("Timeout from FreeDNS API at %s", url)
return False

View File

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

View File

@ -28,7 +28,7 @@ from .util import (
TYPES = Registry()
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['HAP-python==1.1.7']
REQUIREMENTS = ['HAP-python==1.1.8']
CONFIG_SCHEMA = vol.Schema({
@ -102,8 +102,7 @@ def get_accessory(hass, state, aid, config):
aid=aid)
elif state.domain == 'alarm_control_panel':
_LOGGER.debug('Add "%s" as "%s"', state.entity_id,
'SecuritySystem')
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem')
return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
alarm_code=config.get(ATTR_CODE),
aid=aid)
@ -120,6 +119,7 @@ def get_accessory(hass, state, aid, config):
state.name, support_auto, aid=aid)
elif state.domain == 'light':
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light')
return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
elif state.domain == 'switch' or state.domain == 'remote' \

View File

@ -8,8 +8,8 @@ from homeassistant.helpers.event import async_track_state_change
from .const import (
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE,
CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL,
CHAR_NAME, CHAR_SERIAL_NUMBER)
from .util import (
show_setup_message, dismiss_setup_message)
@ -39,15 +39,6 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number)
def override_properties(char, properties=None, valid_values=None):
"""Override characteristic property values and valid values."""
if properties:
char.properties.update(properties)
if valid_values:
char.properties['ValidValues'].update(valid_values)
class HomeAccessory(Accessory):
"""Adapter class for Accessory."""
@ -65,10 +56,10 @@ class HomeAccessory(Accessory):
def run(self):
"""Method called by accessory after driver is started."""
state = self._hass.states.get(self._entity_id)
state = self.hass.states.get(self.entity_id)
self.update_state(new_state=state)
async_track_state_change(
self._hass, self._entity_id, self.update_state)
self.hass, self.entity_id, self.update_state)
class HomeBridge(Bridge):
@ -79,11 +70,10 @@ class HomeBridge(Bridge):
"""Initialize a Bridge object."""
super().__init__(name, **kwargs)
set_accessory_info(self, name, model)
self._hass = hass
self.hass = hass
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
add_preload_service(self, SERV_BRIDGING_STATE)
def setup_message(self):
"""Prevent print of pyhap setup message to terminal."""
@ -92,12 +82,12 @@ class HomeBridge(Bridge):
def add_paired_client(self, client_uuid, client_public):
"""Override super function to dismiss setup message if paired."""
super().add_paired_client(client_uuid, client_public)
dismiss_setup_message(self._hass)
dismiss_setup_message(self.hass)
def remove_paired_client(self, client_uuid):
"""Override super function to show setup message if unpaired."""
super().remove_paired_client(client_uuid)
show_setup_message(self, self._hass)
show_setup_message(self, self.hass)
class HomeDriver(AccessoryDriver):

View File

@ -24,13 +24,16 @@ BRIDGE_NAME = 'Home Assistant'
MANUFACTURER = 'HomeAssistant'
# #### Categories ####
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
CATEGORY_LIGHT = 'LIGHTBULB'
CATEGORY_SENSOR = 'SENSOR'
CATEGORY_SWITCH = 'SWITCH'
CATEGORY_THERMOSTAT = 'THERMOSTAT'
CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING'
# #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation'
SERV_BRIDGING_STATE = 'BridgingState'
SERV_HUMIDITY_SENSOR = 'HumiditySensor'
# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered,
# StatusLowBattery, Name
@ -43,9 +46,8 @@ SERV_WINDOW_COVERING = 'WindowCovering'
# #### Characteristics ####
CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier'
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
CHAR_CATEGORY = 'Category'
CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
CHAR_CURRENT_POSITION = 'CurrentPosition'
@ -54,13 +56,11 @@ CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
CHAR_LINK_QUALITY = 'LinkQuality'
CHAR_MANUFACTURER = 'Manufacturer'
CHAR_MODEL = 'Model'
CHAR_NAME = 'Name'
CHAR_ON = 'On' # boolean
CHAR_POSITION_STATE = 'PositionState'
CHAR_REACHABLE = 'Reachable'
CHAR_SATURATION = 'Saturation' # percent
CHAR_SERIAL_NUMBER = 'SerialNumber'
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'

View File

@ -6,8 +6,8 @@ from homeassistant.components.cover import ATTR_CURRENT_POSITION
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION,
CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
_LOGGER = logging.getLogger(__name__)
@ -20,13 +20,13 @@ class WindowCovering(HomeAccessory):
The cover entity must support: set_cover_position.
"""
def __init__(self, hass, entity_id, display_name, *args, **kwargs):
def __init__(self, hass, entity_id, display_name, **kwargs):
"""Initialize a WindowCovering accessory object."""
super().__init__(display_name, entity_id, 'WINDOW_COVERING',
*args, **kwargs)
super().__init__(display_name, entity_id,
CATEGORY_WINDOW_COVERING, **kwargs)
self._hass = hass
self._entity_id = entity_id
self.hass = hass
self.entity_id = entity_id
self.current_position = None
self.homekit_target = None
@ -48,14 +48,14 @@ class WindowCovering(HomeAccessory):
"""Move cover to value if call came from HomeKit."""
self.char_target_position.set_value(value, should_callback=False)
if value != self.current_position:
_LOGGER.debug('%s: Set position to %d', self._entity_id, value)
_LOGGER.debug('%s: Set position to %d', self.entity_id, value)
self.homekit_target = value
if value > self.current_position:
self.char_position_state.set_value(1)
elif value < self.current_position:
self.char_position_state.set_value(0)
self._hass.components.cover.set_cover_position(
value, self._entity_id)
self.hass.components.cover.set_cover_position(
value, self.entity_id)
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update cover position after state changed."""
@ -63,14 +63,11 @@ class WindowCovering(HomeAccessory):
return
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
if current_position is None:
return
self.current_position = int(current_position)
self.char_current_position.set_value(self.current_position)
if self.homekit_target is None or \
abs(self.current_position - self.homekit_target) < 6:
self.char_target_position.set_value(self.current_position)
self.char_position_state.set_value(2)
self.homekit_target = None
if isinstance(current_position, int):
self.current_position = current_position
self.char_current_position.set_value(self.current_position)
if self.homekit_target is None or \
abs(self.current_position - self.homekit_target) < 6:
self.char_target_position.set_value(self.current_position)
self.char_position_state.set_value(2)
self.homekit_target = None

View File

@ -2,13 +2,14 @@
import logging
from homeassistant.components.light import (
ATTR_HS_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_COLOR)
ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS,
ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS)
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
CATEGORY_LIGHT, SERV_LIGHTBULB,
CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
_LOGGER = logging.getLogger(__name__)
@ -20,25 +21,27 @@ RGB_COLOR = 'rgb_color'
class Light(HomeAccessory):
"""Generate a Light accessory for a light entity.
Currently supports: state, brightness, rgb_color.
Currently supports: state, brightness, color temperature, rgb_color.
"""
def __init__(self, hass, entity_id, name, *args, **kwargs):
def __init__(self, hass, entity_id, name, **kwargs):
"""Initialize a new Light accessory object."""
super().__init__(name, entity_id, CATEGORY_LIGHT, *args, **kwargs)
super().__init__(name, entity_id, CATEGORY_LIGHT, **kwargs)
self._hass = hass
self._entity_id = entity_id
self.hass = hass
self.entity_id = entity_id
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
CHAR_HUE: False, CHAR_SATURATION: False,
RGB_COLOR: False}
CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
self._state = 0
self.chars = []
self._features = self._hass.states.get(self._entity_id) \
self._features = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES)
if self._features & SUPPORT_BRIGHTNESS:
self.chars.append(CHAR_BRIGHTNESS)
if self._features & SUPPORT_COLOR_TEMP:
self.chars.append(CHAR_COLOR_TEMPERATURE)
if self._features & SUPPORT_COLOR:
self.chars.append(CHAR_HUE)
self.chars.append(CHAR_SATURATION)
@ -55,6 +58,18 @@ class Light(HomeAccessory):
.get_characteristic(CHAR_BRIGHTNESS)
self.char_brightness.setter_callback = self.set_brightness
self.char_brightness.value = 0
if CHAR_COLOR_TEMPERATURE in self.chars:
self.char_color_temperature = serv_light \
.get_characteristic(CHAR_COLOR_TEMPERATURE)
self.char_color_temperature.setter_callback = \
self.set_color_temperature
min_mireds = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MIN_MIREDS, 153)
max_mireds = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MAX_MIREDS, 500)
self.char_color_temperature.override_properties({
'minValue': min_mireds, 'maxValue': max_mireds})
self.char_color_temperature.value = min_mireds
if CHAR_HUE in self.chars:
self.char_hue = serv_light.get_characteristic(CHAR_HUE)
self.char_hue.setter_callback = self.set_hue
@ -70,29 +85,36 @@ class Light(HomeAccessory):
if self._state == value:
return
_LOGGER.debug('%s: Set state to %d', self._entity_id, value)
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
self._flag[CHAR_ON] = True
self.char_on.set_value(value, should_callback=False)
if value == 1:
self._hass.components.light.turn_on(self._entity_id)
self.hass.components.light.turn_on(self.entity_id)
elif value == 0:
self._hass.components.light.turn_off(self._entity_id)
self.hass.components.light.turn_off(self.entity_id)
def set_brightness(self, value):
"""Set brightness if call came from HomeKit."""
_LOGGER.debug('%s: Set brightness to %d', self._entity_id, value)
_LOGGER.debug('%s: Set brightness to %d', self.entity_id, value)
self._flag[CHAR_BRIGHTNESS] = True
self.char_brightness.set_value(value, should_callback=False)
if value != 0:
self._hass.components.light.turn_on(
self._entity_id, brightness_pct=value)
self.hass.components.light.turn_on(
self.entity_id, brightness_pct=value)
else:
self._hass.components.light.turn_off(self._entity_id)
self.hass.components.light.turn_off(self.entity_id)
def set_color_temperature(self, value):
"""Set color temperature if call came from HomeKit."""
_LOGGER.debug('%s: Set color temp to %s', self.entity_id, value)
self._flag[CHAR_COLOR_TEMPERATURE] = True
self.char_color_temperature.set_value(value, should_callback=False)
self.hass.components.light.turn_on(self.entity_id, color_temp=value)
def set_saturation(self, value):
"""Set saturation if call came from HomeKit."""
_LOGGER.debug('%s: Set saturation to %d', self._entity_id, value)
_LOGGER.debug('%s: Set saturation to %d', self.entity_id, value)
self._flag[CHAR_SATURATION] = True
self.char_saturation.set_value(value, should_callback=False)
self._saturation = value
@ -100,7 +122,7 @@ class Light(HomeAccessory):
def set_hue(self, value):
"""Set hue if call came from HomeKit."""
_LOGGER.debug('%s: Set hue to %d', self._entity_id, value)
_LOGGER.debug('%s: Set hue to %d', self.entity_id, value)
self._flag[CHAR_HUE] = True
self.char_hue.set_value(value, should_callback=False)
self._hue = value
@ -112,11 +134,11 @@ class Light(HomeAccessory):
if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \
self._flag[CHAR_SATURATION]:
color = (self._hue, self._saturation)
_LOGGER.debug('%s: Set hs_color to %s', self._entity_id, color)
_LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color)
self._flag.update({
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
self._hass.components.light.turn_on(
self._entity_id, hs_color=color)
self.hass.components.light.turn_on(
self.entity_id, hs_color=color)
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update light after state change."""
@ -141,13 +163,25 @@ class Light(HomeAccessory):
should_callback=False)
self._flag[CHAR_BRIGHTNESS] = False
# Handle color temperature
if CHAR_COLOR_TEMPERATURE in self.chars:
color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP)
if not self._flag[CHAR_COLOR_TEMPERATURE] \
and isinstance(color_temperature, int):
self.char_color_temperature.set_value(color_temperature,
should_callback=False)
self._flag[CHAR_COLOR_TEMPERATURE] = False
# Handle Color
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
hue, saturation = new_state.attributes.get(
ATTR_HS_COLOR, (None, None))
if not self._flag[RGB_COLOR] and (
hue != self._hue or saturation != self._saturation):
hue != self._hue or saturation != self._saturation) and \
isinstance(hue, (int, float)) and \
isinstance(saturation, (int, float)):
self.char_hue.set_value(hue, should_callback=False)
self.char_saturation.set_value(saturation,
should_callback=False)
self._hue, self._saturation = (hue, saturation)
self._flag[RGB_COLOR] = False

View File

@ -9,8 +9,8 @@ from homeassistant.const import (
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE,
CHAR_TARGET_SECURITY_STATE)
CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM,
CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE)
_LOGGER = logging.getLogger(__name__)
@ -27,14 +27,13 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
class SecuritySystem(HomeAccessory):
"""Generate an SecuritySystem accessory for an alarm control panel."""
def __init__(self, hass, entity_id, display_name,
alarm_code, *args, **kwargs):
def __init__(self, hass, entity_id, display_name, alarm_code, **kwargs):
"""Initialize a SecuritySystem accessory object."""
super().__init__(display_name, entity_id, 'ALARM_SYSTEM',
*args, **kwargs)
super().__init__(display_name, entity_id,
CATEGORY_ALARM_SYSTEM, **kwargs)
self._hass = hass
self._entity_id = entity_id
self.hass = hass
self.entity_id = entity_id
self._alarm_code = alarm_code
self.flag_target_state = False
@ -52,16 +51,16 @@ class SecuritySystem(HomeAccessory):
def set_security_state(self, value):
"""Move security state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set security state to %d',
self._entity_id, value)
self.entity_id, value)
self.flag_target_state = True
self.char_target_state.set_value(value, should_callback=False)
hass_value = HOMEKIT_TO_HASS[value]
service = STATE_TO_SERVICE[hass_value]
params = {ATTR_ENTITY_ID: self._entity_id}
params = {ATTR_ENTITY_ID: self.entity_id}
if self._alarm_code:
params[ATTR_CODE] = self._alarm_code
self._hass.services.call('alarm_control_panel', service, params)
self.hass.services.call('alarm_control_panel', service, params)
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update security state after state changed."""
@ -76,7 +75,7 @@ class SecuritySystem(HomeAccessory):
self.char_current_state.set_value(current_security_state,
should_callback=False)
_LOGGER.debug('%s: Updated current state to %s (%d)',
self._entity_id, hass_state, current_security_state)
self.entity_id, hass_state, current_security_state)
if not self.flag_target_state:
self.char_target_state.set_value(current_security_state,

View File

@ -5,8 +5,7 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
from . import TYPES
from .accessories import (
HomeAccessory, add_preload_service, override_properties)
from .accessories import HomeAccessory, add_preload_service
from .const import (
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
@ -23,16 +22,16 @@ class TemperatureSensor(HomeAccessory):
Sensor entity must return temperature in °C, °F.
"""
def __init__(self, hass, entity_id, name, *args, **kwargs):
def __init__(self, hass, entity_id, name, **kwargs):
"""Initialize a TemperatureSensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs)
self._hass = hass
self._entity_id = entity_id
self.hass = hass
self.entity_id = entity_id
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE)
override_properties(self.char_temp, PROP_CELSIUS)
self.char_temp.override_properties(properties=PROP_CELSIUS)
self.char_temp.value = 0
self.unit = None
@ -47,7 +46,7 @@ class TemperatureSensor(HomeAccessory):
temperature = temperature_to_homekit(temperature, unit)
self.char_temp.set_value(temperature, should_callback=False)
_LOGGER.debug('%s: Current temperature set to %d°C',
self._entity_id, temperature)
self.entity_id, temperature)
@TYPES.register('HumiditySensor')
@ -58,8 +57,8 @@ class HumiditySensor(HomeAccessory):
"""Initialize a HumiditySensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
self._hass = hass
self._entity_id = entity_id
self.hass = hass
self.entity_id = entity_id
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
self.char_humidity = serv_humidity \
@ -75,4 +74,4 @@ class HumiditySensor(HomeAccessory):
if humidity:
self.char_humidity.set_value(humidity, should_callback=False)
_LOGGER.debug('%s: Percent set to %d%%',
self._entity_id, humidity)
self.entity_id, humidity)

View File

@ -7,7 +7,7 @@ from homeassistant.core import split_entity_id
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import SERV_SWITCH, CHAR_ON
from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON
_LOGGER = logging.getLogger(__name__)
@ -16,12 +16,12 @@ _LOGGER = logging.getLogger(__name__)
class Switch(HomeAccessory):
"""Generate a Switch accessory."""
def __init__(self, hass, entity_id, display_name, *args, **kwargs):
def __init__(self, hass, entity_id, display_name, **kwargs):
"""Initialize a Switch accessory object to represent a remote."""
super().__init__(display_name, entity_id, 'SWITCH', *args, **kwargs)
super().__init__(display_name, entity_id, CATEGORY_SWITCH, **kwargs)
self._hass = hass
self._entity_id = entity_id
self.hass = hass
self.entity_id = entity_id
self._domain = split_entity_id(entity_id)[0]
self.flag_target_state = False
@ -34,12 +34,12 @@ class Switch(HomeAccessory):
def set_state(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state to %s',
self._entity_id, value)
self.entity_id, value)
self.flag_target_state = True
self.char_on.set_value(value, should_callback=False)
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self._hass.services.call(self._domain, service,
{ATTR_ENTITY_ID: self._entity_id})
self.hass.services.call(self._domain, service,
{ATTR_ENTITY_ID: self.entity_id})
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update switch state after state changed."""
@ -49,7 +49,7 @@ class Switch(HomeAccessory):
current_state = (new_state.state == STATE_ON)
if not self.flag_target_state:
_LOGGER.debug('%s: Set current state to %s',
self._entity_id, current_state)
self.entity_id, current_state)
self.char_on.set_value(current_state, should_callback=False)
self.flag_target_state = False

View File

@ -7,12 +7,12 @@ from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
STATE_HEAT, STATE_COOL, STATE_AUTO)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT)
ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from . import TYPES
from .accessories import HomeAccessory, add_preload_service
from .const import (
SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
@ -20,7 +20,6 @@ from .util import temperature_to_homekit, temperature_to_states
_LOGGER = logging.getLogger(__name__)
STATE_OFF = 'off'
UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1}
UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()}
HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1,
@ -32,14 +31,13 @@ HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
class Thermostat(HomeAccessory):
"""Generate a Thermostat accessory for a climate."""
def __init__(self, hass, entity_id, display_name,
support_auto, *args, **kwargs):
def __init__(self, hass, entity_id, display_name, support_auto, **kwargs):
"""Initialize a Thermostat accessory object."""
super().__init__(display_name, entity_id, 'THERMOSTAT',
*args, **kwargs)
super().__init__(display_name, entity_id,
CATEGORY_THERMOSTAT, **kwargs)
self._hass = hass
self._entity_id = entity_id
self.hass = hass
self.entity_id = entity_id
self._call_timer = None
self._unit = TEMP_CELSIUS
@ -101,48 +99,48 @@ class Thermostat(HomeAccessory):
"""Move operation mode to value if call came from HomeKit."""
self.char_target_heat_cool.set_value(value, should_callback=False)
if value in HC_HOMEKIT_TO_HASS:
_LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value)
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
self.heat_cool_flag_target_state = True
hass_value = HC_HOMEKIT_TO_HASS[value]
self._hass.components.climate.set_operation_mode(
operation_mode=hass_value, entity_id=self._entity_id)
self.hass.components.climate.set_operation_mode(
operation_mode=hass_value, entity_id=self.entity_id)
def set_cooling_threshold(self, value):
"""Set cooling threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
self._entity_id, value)
self.entity_id, value)
self.coolingthresh_flag_target_state = True
self.char_cooling_thresh_temp.set_value(value, should_callback=False)
low = self.char_heating_thresh_temp.value
low = temperature_to_states(low, self._unit)
value = temperature_to_states(value, self._unit)
self._hass.components.climate.set_temperature(
entity_id=self._entity_id, target_temp_high=value,
self.hass.components.climate.set_temperature(
entity_id=self.entity_id, target_temp_high=value,
target_temp_low=low)
def set_heating_threshold(self, value):
"""Set heating threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
self._entity_id, value)
self.entity_id, value)
self.heatingthresh_flag_target_state = True
self.char_heating_thresh_temp.set_value(value, should_callback=False)
# Home assistant always wants to set low and high at the same time
high = self.char_cooling_thresh_temp.value
high = temperature_to_states(high, self._unit)
value = temperature_to_states(value, self._unit)
self._hass.components.climate.set_temperature(
entity_id=self._entity_id, target_temp_high=high,
self.hass.components.climate.set_temperature(
entity_id=self.entity_id, target_temp_high=high,
target_temp_low=value)
def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit."""
_LOGGER.debug('%s: Set target temperature to %.2f°C',
self._entity_id, value)
self.entity_id, value)
self.temperature_flag_target_state = True
self.char_target_temp.set_value(value, should_callback=False)
value = temperature_to_states(value, self._unit)
self._hass.components.climate.set_temperature(
temperature=value, entity_id=self._entity_id)
self.hass.components.climate.set_temperature(
temperature=value, entity_id=self.entity_id)
def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update security state after state changed."""

View File

@ -4,31 +4,23 @@ This component provides basic support for the Philips Hue system.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/hue/
"""
import asyncio
import json
import ipaddress
import logging
import os
import async_timeout
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.discovery import SERVICE_HUE
from homeassistant.const import CONF_FILENAME, CONF_HOST
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery, aiohttp_client
from homeassistant import config_entries
from homeassistant.util.json import save_json
from homeassistant.helpers import aiohttp_client, config_validation as cv
from .const import DOMAIN, API_NUPNP
from .bridge import HueBridge
# Loading the config flow file will register the flow
from .config_flow import configured_hosts
REQUIREMENTS = ['aiohue==1.3.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "hue"
SERVICE_HUE_SCENE = "hue_activate_scene"
API_NUPNP = 'https://www.meethue.com/api/nupnp'
CONF_BRIDGES = "bridges"
CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
@ -42,6 +34,7 @@ DEFAULT_ALLOW_HUE_GROUPS = True
BRIDGE_CONFIG_SCHEMA = vol.Schema({
# Validate as IP address and then convert back to a string.
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string),
# This is for legacy reasons and is only used for importing auth.
vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
vol.Optional(CONF_ALLOW_UNREACHABLE,
default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
@ -56,19 +49,6 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
ATTR_GROUP_NAME = "group_name"
ATTR_SCENE_NAME = "scene_name"
SCENE_SCHEMA = vol.Schema({
vol.Required(ATTR_GROUP_NAME): cv.string,
vol.Required(ATTR_SCENE_NAME): cv.string,
})
CONFIG_INSTRUCTIONS = """
Press the button on the bridge to register Philips Hue with Home Assistant.
![Location of button on bridge](/static/images/config_philips_hue.jpg)
"""
async def async_setup(hass, config):
"""Set up the Hue platform."""
@ -76,20 +56,8 @@ async def async_setup(hass, config):
if conf is None:
conf = {}
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
async def async_bridge_discovered(service, discovery_info):
"""Dispatcher for Hue discovery events."""
# Ignore emulated hue
if "HASS Bridge" in discovery_info.get('name', ''):
return
await async_setup_bridge(
hass, discovery_info['host'],
'phue-{}.conf'.format(discovery_info['serial']))
discovery.async_listen(hass, SERVICE_HUE, async_bridge_discovered)
hass.data[DOMAIN] = {}
configured = configured_hosts(hass)
# User has configured bridges
if CONF_BRIDGES in conf:
@ -103,12 +71,19 @@ async def async_setup(hass, config):
async with websession.get(API_NUPNP) as req:
hosts = await req.json()
# Run through config schema to populate defaults
bridges = [BRIDGE_CONFIG_SCHEMA({
CONF_HOST: entry['internalipaddress'],
CONF_FILENAME: '.hue_{}.conf'.format(entry['id']),
}) for entry in hosts]
bridges = []
for entry in hosts:
# Filter out already configured hosts
if entry['internalipaddress'] in configured:
continue
# Run through config schema to populate defaults
bridges.append(BRIDGE_CONFIG_SCHEMA({
CONF_HOST: entry['internalipaddress'],
# Careful with using entry['id'] for other reasons. The
# value is in lowercase but is returned uppercase from hub.
CONF_FILENAME: '.hue_{}.conf'.format(entry['id']),
}))
else:
# Component not specified in config, we're loaded via discovery
bridges = []
@ -116,277 +91,43 @@ async def async_setup(hass, config):
if not bridges:
return True
await asyncio.wait([
async_setup_bridge(
hass, bridge[CONF_HOST], bridge[CONF_FILENAME],
bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS]
) for bridge in bridges
])
for bridge_conf in bridges:
host = bridge_conf[CONF_HOST]
# Store config in hass.data so the config entry can find it
hass.data[DOMAIN][host] = bridge_conf
# If configured, the bridge will be set up during config entry phase
if host in configured:
continue
# No existing config entry found, try importing it or trigger link
# config flow if no existing auth. Because we're inside the setup of
# this component we'll have to use hass.async_add_job to avoid a
# deadlock: creating a config entry will set up the component but the
# setup would block till the entry is created!
hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
'host': bridge_conf[CONF_HOST],
'path': bridge_conf[CONF_FILENAME],
}
))
return True
async def async_setup_bridge(
hass, host, filename=None,
allow_unreachable=DEFAULT_ALLOW_UNREACHABLE,
allow_hue_groups=DEFAULT_ALLOW_HUE_GROUPS,
username=None):
"""Set up a given Hue bridge."""
assert filename or username, 'Need to pass at least a username or filename'
# Only register a device once
if host in hass.data[DOMAIN]:
return
if username is None:
username = await hass.async_add_job(
_find_username_from_config, hass, filename)
bridge = HueBridge(host, hass, filename, username, allow_unreachable,
allow_hue_groups)
await bridge.async_setup()
def _find_username_from_config(hass, filename):
"""Load username from config."""
path = hass.config.path(filename)
if not os.path.isfile(path):
return None
with open(path) as inp:
return list(json.load(inp).values())[0]['username']
class HueBridge(object):
"""Manages a single Hue bridge."""
def __init__(self, host, hass, filename, username,
allow_unreachable=False, allow_groups=True):
"""Initialize the system."""
self.host = host
self.hass = hass
self.filename = filename
self.username = username
self.allow_unreachable = allow_unreachable
self.allow_groups = allow_groups
self.available = True
self.config_request_id = None
self.api = None
async def async_setup(self):
"""Set up a phue bridge based on host parameter."""
import aiohue
api = aiohue.Bridge(
self.host,
username=self.username,
websession=aiohttp_client.async_get_clientsession(self.hass)
)
try:
with async_timeout.timeout(5):
# Initialize bridge and validate our username
if not self.username:
await api.create_user('home-assistant')
await api.initialize()
except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
_LOGGER.warning("Connected to Hue at %s but not registered.",
self.host)
self.async_request_configuration()
return
except (asyncio.TimeoutError, aiohue.RequestError):
_LOGGER.error("Error connecting to the Hue bridge at %s",
self.host)
return
except aiohue.AiohueException:
_LOGGER.exception('Unknown Hue linking error occurred')
self.async_request_configuration()
return
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unknown error connecting with Hue bridge at %s",
self.host)
return
self.hass.data[DOMAIN][self.host] = self
# If we came here and configuring this host, mark as done
if self.config_request_id:
request_id = self.config_request_id
self.config_request_id = None
self.hass.components.configurator.async_request_done(request_id)
self.username = api.username
# Save config file
await self.hass.async_add_job(
save_json, self.hass.config.path(self.filename),
{self.host: {'username': api.username}})
self.api = api
self.hass.async_add_job(discovery.async_load_platform(
self.hass, 'light', DOMAIN,
{'host': self.host}))
self.hass.services.async_register(
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
schema=SCENE_SCHEMA)
@callback
def async_request_configuration(self):
"""Request configuration steps from the user."""
configurator = self.hass.components.configurator
# We got an error if this method is called while we are configuring
if self.config_request_id:
configurator.async_notify_errors(
self.config_request_id,
"Failed to register, please try again.")
return
async def config_callback(data):
"""Callback for configurator data."""
await self.async_setup()
self.config_request_id = configurator.async_request_config(
"Philips Hue", config_callback,
description=CONFIG_INSTRUCTIONS,
entity_picture="/static/images/logo_philips_hue.png",
submit_caption="I have pressed the button"
)
async def hue_activate_scene(self, call, updated=False):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
group = next(
(group for group in self.api.groups.values()
if group.name == group_name), None)
scene_id = next(
(scene.id for scene in self.api.scenes.values()
if scene.name == scene_name), None)
# If we can't find it, fetch latest info.
if not updated and (group is None or scene_id is None):
await self.api.groups.update()
await self.api.scenes.update()
await self.hue_activate_scene(call, updated=True)
return
if group is None:
_LOGGER.warning('Unable to find group %s', group_name)
return
if scene_id is None:
_LOGGER.warning('Unable to find scene %s', scene_name)
return
await group.set_action(scene=scene_id)
@config_entries.HANDLERS.register(DOMAIN)
class HueFlowHandler(config_entries.ConfigFlowHandler):
"""Handle a Hue config flow."""
VERSION = 1
def __init__(self):
"""Initialize the Hue flow."""
self.host = None
@property
def _websession(self):
"""Return a websession.
Cannot assign in init because hass variable is not set yet.
"""
return aiohttp_client.async_get_clientsession(self.hass)
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
from aiohue.discovery import discover_nupnp
if user_input is not None:
self.host = user_input['host']
return await self.async_step_link()
try:
with async_timeout.timeout(5):
bridges = await discover_nupnp(websession=self._websession)
except asyncio.TimeoutError:
return self.async_abort(
reason='discover_timeout'
)
if not bridges:
return self.async_abort(
reason='no_bridges'
)
# Find already configured hosts
configured_hosts = set(
entry.data['host'] for entry
in self.hass.config_entries.async_entries(DOMAIN))
hosts = [bridge.host for bridge in bridges
if bridge.host not in configured_hosts]
if not hosts:
return self.async_abort(
reason='all_configured'
)
elif len(hosts) == 1:
self.host = hosts[0]
return await self.async_step_link()
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({
vol.Required('host'): vol.In(hosts)
})
)
async def async_step_link(self, user_input=None):
"""Attempt to link with the Hue bridge."""
import aiohue
errors = {}
if user_input is not None:
bridge = aiohue.Bridge(self.host, websession=self._websession)
try:
with async_timeout.timeout(5):
# Create auth token
await bridge.create_user('home-assistant')
# Fetches name and id
await bridge.initialize()
except (asyncio.TimeoutError, aiohue.RequestError,
aiohue.LinkButtonNotPressed):
errors['base'] = 'register_failed'
except aiohue.AiohueException:
errors['base'] = 'linking'
_LOGGER.exception('Unknown Hue linking error occurred')
else:
return self.async_create_entry(
title=bridge.config.name,
data={
'host': bridge.host,
'bridge_id': bridge.config.bridgeid,
'username': bridge.username,
}
)
return self.async_show_form(
step_id='link',
errors=errors,
)
async def async_setup_entry(hass, entry):
"""Set up a bridge for a config entry."""
await async_setup_bridge(hass, entry.data['host'],
username=entry.data['username'])
return True
"""Set up a bridge from a config entry."""
host = entry.data['host']
config = hass.data[DOMAIN].get(host)
if config is None:
allow_unreachable = DEFAULT_ALLOW_UNREACHABLE
allow_groups = DEFAULT_ALLOW_HUE_GROUPS
else:
allow_unreachable = config[CONF_ALLOW_UNREACHABLE]
allow_groups = config[CONF_ALLOW_HUE_GROUPS]
bridge = HueBridge(hass, entry, allow_unreachable, allow_groups)
hass.data[DOMAIN][host] = bridge
return await bridge.async_setup()

View File

@ -0,0 +1,148 @@
"""Code to handle a Hue bridge."""
import asyncio
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.helpers import aiohttp_client, config_validation as cv
from .const import DOMAIN, LOGGER
from .errors import AuthenticationRequired, CannotConnect
SERVICE_HUE_SCENE = "hue_activate_scene"
ATTR_GROUP_NAME = "group_name"
ATTR_SCENE_NAME = "scene_name"
SCENE_SCHEMA = vol.Schema({
vol.Required(ATTR_GROUP_NAME): cv.string,
vol.Required(ATTR_SCENE_NAME): cv.string,
})
class HueBridge(object):
"""Manages a single Hue bridge."""
def __init__(self, hass, config_entry, allow_unreachable, allow_groups):
"""Initialize the system."""
self.config_entry = config_entry
self.hass = hass
self.allow_unreachable = allow_unreachable
self.allow_groups = allow_groups
self.available = True
self.api = None
@property
def host(self):
"""Return the host of this bridge."""
return self.config_entry.data['host']
async def async_setup(self, tries=0):
"""Set up a phue bridge based on host parameter."""
host = self.host
try:
self.api = await get_bridge(
self.hass, host,
self.config_entry.data['username']
)
except AuthenticationRequired:
# usernames can become invalid if hub is reset or user removed.
# We are going to fail the config entry setup and initiate a new
# linking procedure. When linking succeeds, it will remove the
# old config entry.
self.hass.async_add_job(self.hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
'host': host,
}
))
return False
except CannotConnect:
retry_delay = 2 ** (tries + 1)
LOGGER.error("Error connecting to the Hue bridge at %s. Retrying "
"in %d seconds", host, retry_delay)
async def retry_setup(_now):
"""Retry setup."""
if await self.async_setup(tries + 1):
# This feels hacky, we should find a better way to do this
self.config_entry.state = config_entries.ENTRY_STATE_LOADED
# Unhandled edge case: cancel this if we discover bridge on new IP
self.hass.helpers.event.async_call_later(retry_delay, retry_setup)
return False
except Exception: # pylint: disable=broad-except
LOGGER.exception('Unknown error connecting with Hue bridge at %s',
host)
return False
self.hass.async_add_job(
self.hass.helpers.discovery.async_load_platform(
'light', DOMAIN, {'host': host}))
self.hass.services.async_register(
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
schema=SCENE_SCHEMA)
return True
async def hue_activate_scene(self, call, updated=False):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME]
group = next(
(group for group in self.api.groups.values()
if group.name == group_name), None)
scene_id = next(
(scene.id for scene in self.api.scenes.values()
if scene.name == scene_name), None)
# If we can't find it, fetch latest info.
if not updated and (group is None or scene_id is None):
await self.api.groups.update()
await self.api.scenes.update()
await self.hue_activate_scene(call, updated=True)
return
if group is None:
LOGGER.warning('Unable to find group %s', group_name)
return
if scene_id is None:
LOGGER.warning('Unable to find scene %s', scene_name)
return
await group.set_action(scene=scene_id)
async def get_bridge(hass, host, username=None):
"""Create a bridge object and verify authentication."""
import aiohue
bridge = aiohue.Bridge(
host, username=username,
websession=aiohttp_client.async_get_clientsession(hass)
)
try:
with async_timeout.timeout(5):
# Create username if we don't have one
if not username:
await bridge.create_user('home-assistant')
# Initialize bridge (and validate our username)
await bridge.initialize()
return bridge
except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized):
LOGGER.warning("Connected to Hue at %s but not registered.", host)
raise AuthenticationRequired
except (asyncio.TimeoutError, aiohue.RequestError):
LOGGER.error("Error connecting to the Hue bridge at %s", host)
raise CannotConnect
except aiohue.AiohueException:
LOGGER.exception('Unknown Hue linking error occurred')
raise AuthenticationRequired

View File

@ -0,0 +1,235 @@
"""Config flow to configure Philips Hue."""
import asyncio
import json
import os
import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from .bridge import get_bridge
from .const import DOMAIN, LOGGER
from .errors import AuthenticationRequired, CannotConnect
@callback
def configured_hosts(hass):
"""Return a set of the configured hosts."""
return set(entry.data['host'] for entry
in hass.config_entries.async_entries(DOMAIN))
def _find_username_from_config(hass, filename):
"""Load username from config.
This was a legacy way of configuring Hue until Home Assistant 0.67.
"""
path = hass.config.path(filename)
if not os.path.isfile(path):
return None
with open(path) as inp:
try:
return list(json.load(inp).values())[0]['username']
except ValueError:
# If we get invalid JSON
return None
@config_entries.HANDLERS.register(DOMAIN)
class HueFlowHandler(config_entries.ConfigFlowHandler):
"""Handle a Hue config flow."""
VERSION = 1
def __init__(self):
"""Initialize the Hue flow."""
self.host = None
async def async_step_init(self, user_input=None):
"""Handle a flow start."""
from aiohue.discovery import discover_nupnp
if user_input is not None:
self.host = user_input['host']
return await self.async_step_link()
websession = aiohttp_client.async_get_clientsession(self.hass)
try:
with async_timeout.timeout(5):
bridges = await discover_nupnp(websession=websession)
except asyncio.TimeoutError:
return self.async_abort(
reason='discover_timeout'
)
if not bridges:
return self.async_abort(
reason='no_bridges'
)
# Find already configured hosts
configured = configured_hosts(self.hass)
hosts = [bridge.host for bridge in bridges
if bridge.host not in configured]
if not hosts:
return self.async_abort(
reason='all_configured'
)
elif len(hosts) == 1:
self.host = hosts[0]
return await self.async_step_link()
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({
vol.Required('host'): vol.In(hosts)
})
)
async def async_step_link(self, user_input=None):
"""Attempt to link with the Hue bridge.
Given a configured host, will ask the user to press the link button
to connect to the bridge.
"""
errors = {}
# We will always try linking in case the user has already pressed
# the link button.
try:
bridge = await get_bridge(
self.hass, self.host, username=None
)
return await self._entry_from_bridge(bridge)
except AuthenticationRequired:
errors['base'] = 'register_failed'
except CannotConnect:
LOGGER.error("Error connecting to the Hue bridge at %s", self.host)
errors['base'] = 'linking'
except Exception: # pylint: disable=broad-except
LOGGER.exception(
'Unknown error connecting with Hue bridge at %s',
self.host)
errors['base'] = 'linking'
# If there was no user input, do not show the errors.
if user_input is None:
errors = {}
return self.async_show_form(
step_id='link',
errors=errors,
)
async def async_step_discovery(self, discovery_info):
"""Handle a discovered Hue bridge.
This flow is triggered by the discovery component. It will check if the
host is already configured and delegate to the import step if not.
"""
# Filter out emulated Hue
if "HASS Bridge" in discovery_info.get('name', ''):
return self.async_abort(reason='already_configured')
host = discovery_info.get('host')
if host in configured_hosts(self.hass):
return self.async_abort(reason='already_configured')
# This value is based off host/description.xml and is, weirdly, missing
# 4 characters in the middle of the serial compared to results returned
# from the NUPNP API or when querying the bridge API for bridgeid.
# (on first gen Hue hub)
serial = discovery_info.get('serial')
return await self.async_step_import({
'host': host,
# This format is the legacy format that Hue used for discovery
'path': 'phue-{}.conf'.format(serial)
})
async def async_step_import(self, import_info):
"""Import a new bridge as a config entry.
Will read authentication from Phue config file if available.
This flow is triggered by `async_setup` for both configured and
discovered bridges. Triggered for any bridge that does not have a
config entry yet (based on host).
This flow is also triggered by `async_step_discovery`.
If an existing config file is found, we will validate the credentials
and create an entry. Otherwise we will delegate to `link` step which
will ask user to link the bridge.
"""
host = import_info['host']
path = import_info.get('path')
if path is not None:
username = await self.hass.async_add_job(
_find_username_from_config, self.hass,
self.hass.config.path(path))
else:
username = None
try:
bridge = await get_bridge(
self.hass, host, username
)
LOGGER.info('Imported authentication for %s from %s', host, path)
return await self._entry_from_bridge(bridge)
except AuthenticationRequired:
self.host = host
LOGGER.info('Invalid authentication for %s, requesting link.',
host)
return await self.async_step_link()
except CannotConnect:
LOGGER.error("Error connecting to the Hue bridge at %s", host)
return self.async_abort(reason='cannot_connect')
except Exception: # pylint: disable=broad-except
LOGGER.exception('Unknown error connecting with Hue bridge at %s',
host)
return self.async_abort(reason='unknown')
async def _entry_from_bridge(self, bridge):
"""Return a config entry from an initialized bridge."""
# Remove all other entries of hubs with same ID or host
host = bridge.host
bridge_id = bridge.config.bridgeid
same_hub_entries = [entry.entry_id for entry
in self.hass.config_entries.async_entries(DOMAIN)
if entry.data['bridge_id'] == bridge_id or
entry.data['host'] == host]
if same_hub_entries:
await asyncio.wait([self.hass.config_entries.async_remove(entry_id)
for entry_id in same_hub_entries])
return self.async_create_entry(
title=bridge.config.name,
data={
'host': host,
'bridge_id': bridge_id,
'username': bridge.username,
}
)

View File

@ -0,0 +1,6 @@
"""Constants for the Hue component."""
import logging
LOGGER = logging.getLogger('homeassistant.components.hue')
DOMAIN = "hue"
API_NUPNP = 'https://www.meethue.com/api/nupnp'

View File

@ -0,0 +1,14 @@
"""Errors for the Hue component."""
from homeassistant.exceptions import HomeAssistantError
class HueException(HomeAssistantError):
"""Base class for Hue exceptions."""
class CannotConnect(HueException):
"""Unable to connect to the bridge."""
class AuthenticationRequired(HueException):
"""Unknown error occurred."""

View File

@ -20,7 +20,10 @@
"abort": {
"discover_timeout": "Unable to discover Hue bridges",
"no_bridges": "No Philips Hue bridges discovered",
"all_configured": "All Philips Hue bridges are already configured"
"all_configured": "All Philips Hue bridges are already configured",
"unknown": "Unknown error occurred",
"cannot_connect": "Unable to connect to the bridge",
"already_configured": "Bridge is already configured"
}
}
}

View File

@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['insteonplm==0.8.3']
REQUIREMENTS = ['insteonplm==0.8.6']
_LOGGER = logging.getLogger(__name__)

View File

@ -457,12 +457,14 @@ class Light(ToggleEntity):
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
# Default to the Philips Hue value that HA has always assumed
return 154
# https://developers.meethue.com/documentation/core-concepts
return 153
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
# Default to the Philips Hue value that HA has always assumed
# https://developers.meethue.com/documentation/core-concepts
return 500
@property

View File

@ -300,8 +300,14 @@ class HueLight(Light):
command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
if ATTR_HS_COLOR in kwargs:
command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
if self.is_osram:
command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535)
command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255)
else:
# Philips hue bulb models respond differently to hue/sat
# requests, so we convert to XY first to ensure a consistent
# color.
command['xy'] = color.color_hs_to_xy(*kwargs[ATTR_HS_COLOR])
elif ATTR_COLOR_TEMP in kwargs:
temp = kwargs[ATTR_COLOR_TEMP]
command['ct'] = max(self.min_mireds, min(temp, self.max_mireds))

View File

@ -79,7 +79,7 @@ class IGloLamp(Light):
@property
def hs_color(self):
"""Return the hs value."""
return color_util.color_RGB_to_hsv(*self._lamp.state()['rgb'])
return color_util.color_RGB_to_hs(*self._lamp.state()['rgb'])
@property
def effect(self):

View File

@ -15,8 +15,9 @@ import homeassistant.util.color as color_util
SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MySensors platform for lights."""
async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the mysensors platform for lights."""
device_class_map = {
'S_DIMMER': MySensorsLightDimmer,
'S_RGB_LIGHT': MySensorsLightRGB,
@ -24,7 +25,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
}
mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, device_class_map,
add_devices=add_devices)
async_add_devices=async_add_devices)
class MySensorsLight(mysensors.MySensorsEntity, Light):
@ -140,12 +141,12 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
self._values[value_type] = STATE_OFF
self.schedule_update_ha_state()
def _update_light(self):
def _async_update_light(self):
"""Update the controller with values from light child."""
value_type = self.gateway.const.SetReq.V_LIGHT
self._state = self._values[value_type] == STATE_ON
def _update_dimmer(self):
def _async_update_dimmer(self):
"""Update the controller with values from dimmer child."""
value_type = self.gateway.const.SetReq.V_DIMMER
if value_type in self._values:
@ -153,7 +154,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
if self._brightness == 0:
self._state = False
def _update_rgb_or_w(self):
def _async_update_rgb_or_w(self):
"""Update the controller with values from RGB or RGBW child."""
value = self._values[self.value_type]
color_list = rgb_hex_to_rgb_list(value)
@ -177,11 +178,11 @@ class MySensorsLightDimmer(MySensorsLight):
if self.gateway.optimistic:
self.schedule_update_ha_state()
def update(self):
async def async_update(self):
"""Update the controller with the latest value from a sensor."""
super().update()
self._update_light()
self._update_dimmer()
await super().async_update()
self._async_update_light()
self._async_update_dimmer()
class MySensorsLightRGB(MySensorsLight):
@ -203,12 +204,12 @@ class MySensorsLightRGB(MySensorsLight):
if self.gateway.optimistic:
self.schedule_update_ha_state()
def update(self):
async def async_update(self):
"""Update the controller with the latest value from a sensor."""
super().update()
self._update_light()
self._update_dimmer()
self._update_rgb_or_w()
await super().async_update()
self._async_update_light()
self._async_update_dimmer()
self._async_update_rgb_or_w()
class MySensorsLightRGBW(MySensorsLightRGB):

View File

@ -15,7 +15,7 @@ from homeassistant.components.light import (
ATTR_HS_COLOR)
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN
REQUIREMENTS = ['python-mystrom==0.3.8']
REQUIREMENTS = ['python-mystrom==0.4.2']
_LOGGER = logging.getLogger(__name__)
@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the myStrom Light platform."""
from pymystrom import MyStromBulb
from pymystrom.bulb import MyStromBulb
from pymystrom.exceptions import MyStromConnectionError
host = config.get(CONF_HOST)

View File

@ -0,0 +1,153 @@
"""
Support for Nanoleaf Aurora platform.
Based in large parts upon Software-2's ha-aurora and fully
reliant on Software-2's nanoleaf-aurora Python Library, see
https://github.com/software-2/ha-aurora as well as
https://github.com/software-2/nanoleaf
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.nanoleaf_aurora/
"""
import logging
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR,
SUPPORT_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
SUPPORT_COLOR, PLATFORM_SCHEMA, Light)
from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.util import color as color_util
from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin
REQUIREMENTS = ['nanoleaf==0.4.1']
SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
SUPPORT_COLOR)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): cv.string,
vol.Optional(CONF_NAME, default='Aurora'): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Nanoleaf Aurora device."""
import nanoleaf
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
token = config.get(CONF_TOKEN)
aurora_light = nanoleaf.Aurora(host, token)
aurora_light.hass_name = name
if aurora_light.on is None:
_LOGGER.error("Could not connect to \
Nanoleaf Aurora: %s on %s", name, host)
add_devices([AuroraLight(aurora_light)], True)
class AuroraLight(Light):
"""Representation of a Nanoleaf Aurora."""
def __init__(self, light):
"""Initialize an Aurora."""
self._brightness = None
self._color_temp = None
self._effect = None
self._effects_list = None
self._light = light
self._name = light.hass_name
self._hs_color = None
self._state = None
@property
def brightness(self):
"""Return the brightness of the light."""
if self._brightness is not None:
return int(self._brightness * 2.55)
return None
@property
def color_temp(self):
"""Return the current color temperature."""
if self._color_temp is not None:
return color_util.color_temperature_kelvin_to_mired(
self._color_temp)
return None
@property
def effect(self):
"""Return the current effect."""
return self._effect
@property
def effect_list(self):
"""Return the list of supported effects."""
return self._effects_list
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return "mdi:triangle-outline"
@property
def is_on(self):
"""Return true if light is on."""
return self._state
@property
def hs_color(self):
"""Return the color in HS."""
return self._hs_color
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_AURORA
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
self._light.on = True
brightness = kwargs.get(ATTR_BRIGHTNESS)
hs_color = kwargs.get(ATTR_HS_COLOR)
color_temp_mired = kwargs.get(ATTR_COLOR_TEMP)
effect = kwargs.get(ATTR_EFFECT)
if hs_color:
hue, saturation = hs_color
self._light.hue = int(hue)
self._light.saturation = int(saturation)
if color_temp_mired:
self._light.color_temperature = mired_to_kelvin(color_temp_mired)
if brightness:
self._light.brightness = int(brightness / 2.55)
if effect:
self._light.effect = effect
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
self._light.on = False
def update(self):
"""Fetch new state data for this light.
This is the only method that should fetch new data for Home Assistant.
"""
self._brightness = self._light.brightness
self._color_temp = self._light.color_temperature
self._effect = self._light.effect
self._effects_list = self._light.effects_list
self._hs_color = self._light.hue, self._light.saturation
self._state = self._light.on

View File

@ -4,21 +4,32 @@ Support for Qwikswitch Relays and Dimmers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.qwikswitch/
"""
import logging
from homeassistant.components.qwikswitch import (
QSToggleEntity, DOMAIN as QWIKSWITCH)
from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light
import homeassistant.components.qwikswitch as qwikswitch
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['qwikswitch']
DEPENDENCIES = [QWIKSWITCH]
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the lights from the main Qwikswitch component."""
async def async_setup_platform(hass, _, add_devices, discovery_info=None):
"""Add lights from the main Qwikswitch component."""
if discovery_info is None:
_LOGGER.error("Configure Qwikswitch component failed")
return False
return
add_devices(qwikswitch.QSUSB['light'])
return True
qsusb = hass.data[QWIKSWITCH]
devs = [QSLight(qsid, qsusb) for qsid in discovery_info[QWIKSWITCH]]
add_devices(devs)
class QSLight(QSToggleEntity, Light):
"""Light based on a Qwikswitch relay/dimmer module."""
@property
def brightness(self):
"""Return the brightness of this light (0-255)."""
return self._qsusb[self.qsid, 1] if self._dim else None
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS if self._dim else 0

View File

@ -15,6 +15,9 @@ turn_on:
color_name:
description: A human readable color name.
example: 'red'
hs_color:
description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100.
example: '[300, 70]'
xy_color:
description: Color for the light in XY-format.
example: '[0.52, 0.43]'
@ -179,3 +182,13 @@ xiaomi_miio_set_delayed_turn_off:
time_period:
description: Time period for the delayed turn off.
example: "5, '0:05', {'minutes': 5}"
yeelight_set_mode:
description: Set a operation mode.
fields:
entity_id:
description: Name of the light entity.
example: 'light.yeelight'
mode:
description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'.
example: 'moonlight'

View File

@ -4,11 +4,9 @@ Support for the IKEA Tradfri platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.tradfri/
"""
import asyncio
import logging
from homeassistant.core import callback
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION,
SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP,
@ -17,20 +15,20 @@ from homeassistant.components.light import \
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA
from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \
KEY_API
from homeassistant.util import color as color_util
import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__)
ATTR_TRANSITION_TIME = 'transition_time'
DEPENDENCIES = ['tradfri']
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
IKEA = 'IKEA of Sweden'
TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager'
SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION)
ALLOWED_TEMPERATURES = {IKEA}
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async def async_setup_platform(hass, config,
async_add_devices, discovery_info=None):
"""Set up the IKEA Tradfri Light platform."""
if discovery_info is None:
return
@ -40,41 +38,43 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
gateway = hass.data[KEY_GATEWAY][gateway_id]
devices_command = gateway.get_devices()
devices_commands = yield from api(devices_command)
devices = yield from api(devices_commands)
devices_commands = await api(devices_command)
devices = await api(devices_commands)
lights = [dev for dev in devices if dev.has_light_control]
if lights:
async_add_devices(TradfriLight(light, api) for light in lights)
async_add_devices(
TradfriLight(light, api, gateway_id) for light in lights)
allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id]
if allow_tradfri_groups:
groups_command = gateway.get_groups()
groups_commands = yield from api(groups_command)
groups = yield from api(groups_commands)
groups_commands = await api(groups_command)
groups = await api(groups_commands)
if groups:
async_add_devices(TradfriGroup(group, api) for group in groups)
async_add_devices(
TradfriGroup(group, api, gateway_id) for group in groups)
class TradfriGroup(Light):
"""The platform class required by hass."""
def __init__(self, light, api):
def __init__(self, group, api, gateway_id):
"""Initialize a Group."""
self._api = api
self._group = light
self._name = light.name
self._unique_id = "group-{}-{}".format(gateway_id, group.id)
self._group = group
self._name = group.name
self._refresh(light)
self._refresh(group)
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Start thread when added to hass."""
self._async_start_observe()
@property
def should_poll(self):
"""No polling needed for tradfri group."""
return False
def unique_id(self):
"""Return unique ID for this group."""
return self._unique_id
@property
def supported_features(self):
@ -96,13 +96,11 @@ class TradfriGroup(Light):
"""Return the brightness of the group lights."""
return self._group.dimmer
@asyncio.coroutine
def async_turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Instruct the group lights to turn off."""
yield from self._api(self._group.set_state(0))
await self._api(self._group.set_state(0))
@asyncio.coroutine
def async_turn_on(self, **kwargs):
async def async_turn_on(self, **kwargs):
"""Instruct the group lights to turn on, or dim."""
keys = {}
if ATTR_TRANSITION in kwargs:
@ -112,16 +110,16 @@ class TradfriGroup(Light):
if kwargs[ATTR_BRIGHTNESS] == 255:
kwargs[ATTR_BRIGHTNESS] = 254
yield from self._api(
await self._api(
self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys))
else:
yield from self._api(self._group.set_state(1))
await self._api(self._group.set_state(1))
@callback
def _async_start_observe(self, exc=None):
"""Start observation of light."""
# pylint: disable=import-error
from pytradfri.error import PyTradFriError
from pytradfri.error import PytradfriError
if exc:
_LOGGER.warning("Observation failed for %s", self._name,
exc_info=exc)
@ -131,7 +129,7 @@ class TradfriGroup(Light):
err_callback=self._async_start_observe,
duration=0)
self.hass.async_add_job(self._api(cmd))
except PyTradFriError as err:
except PytradfriError as err:
_LOGGER.warning("Observation failed, trying again", exc_info=err)
self._async_start_observe()
@ -146,54 +144,44 @@ class TradfriGroup(Light):
self._refresh(tradfri_device)
self.async_schedule_update_ha_state()
async def async_update(self):
"""Fetch new state data for the group."""
await self._api(self._group.update())
class TradfriLight(Light):
"""The platform class required by Home Assistant."""
def __init__(self, light, api):
def __init__(self, light, api, gateway_id):
"""Initialize a Light."""
self._api = api
self._unique_id = "light-{}-{}".format(gateway_id, light.id)
self._light = None
self._light_control = None
self._light_data = None
self._name = None
self._hs_color = None
self._features = SUPPORTED_FEATURES
self._temp_supported = False
self._available = True
self._refresh(light)
@property
def unique_id(self):
"""Return unique ID for light."""
return self._unique_id
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
if self._light_control.max_kelvin is not None:
return color_util.color_temperature_kelvin_to_mired(
self._light_control.max_kelvin
)
return self._light_control.min_mireds
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
if self._light_control.min_kelvin is not None:
return color_util.color_temperature_kelvin_to_mired(
self._light_control.min_kelvin
)
return self._light_control.max_mireds
@property
def device_state_attributes(self):
"""Return the devices' state attributes."""
info = self._light.device_info
attrs = {}
if info.battery_level is not None:
attrs[ATTR_BATTERY_LEVEL] = info.battery_level
return attrs
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Start thread when added to hass."""
self._async_start_observe()
@ -229,64 +217,87 @@ class TradfriLight(Light):
@property
def color_temp(self):
"""Return the CT color value in mireds."""
kelvin_color = self._light_data.kelvin_color_inferred
if kelvin_color is not None:
return color_util.color_temperature_kelvin_to_mired(
kelvin_color
)
"""Return the color temp value in mireds."""
return self._light_data.color_temp
@property
def hs_color(self):
"""HS color of the light."""
return self._hs_color
if self._light_control.can_set_color:
hsbxy = self._light_data.hsb_xy_color
hue = hsbxy[0] / (65535 / 360)
sat = hsbxy[1] / (65279 / 100)
if hue is not None and sat is not None:
return hue, sat
@asyncio.coroutine
def async_turn_off(self, **kwargs):
async def async_turn_off(self, **kwargs):
"""Instruct the light to turn off."""
yield from self._api(self._light_control.set_state(False))
await self._api(self._light_control.set_state(False))
@asyncio.coroutine
def async_turn_on(self, **kwargs):
"""
Instruct the light to turn on.
After adding "self._light_data.hexcolor is not None"
for ATTR_HS_COLOR, this also supports Philips Hue bulbs.
"""
if ATTR_HS_COLOR in kwargs and self._light_data.hex_color is not None:
rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
yield from self._api(
self._light.light_control.set_rgb_color(*rgb))
elif ATTR_COLOR_TEMP in kwargs and \
self._light_data.hex_color is not None and \
self._temp_supported:
kelvin = color_util.color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP])
yield from self._api(
self._light_control.set_kelvin_color(kelvin))
keys = {}
async def async_turn_on(self, **kwargs):
"""Instruct the light to turn on."""
params = {}
transition_time = None
if ATTR_TRANSITION in kwargs:
keys['transition_time'] = int(kwargs[ATTR_TRANSITION]) * 10
transition_time = int(kwargs[ATTR_TRANSITION]) * 10
if ATTR_BRIGHTNESS in kwargs:
if kwargs[ATTR_BRIGHTNESS] == 255:
kwargs[ATTR_BRIGHTNESS] = 254
brightness = kwargs.get(ATTR_BRIGHTNESS)
yield from self._api(
self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS],
**keys))
if brightness is not None:
if brightness > 254:
brightness = 254
elif brightness < 0:
brightness = 0
if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color:
params[ATTR_BRIGHTNESS] = brightness
hue = int(kwargs[ATTR_HS_COLOR][0] * (65535 / 360))
sat = int(kwargs[ATTR_HS_COLOR][1] * (65279 / 100))
await self._api(
self._light_control.set_hsb(hue, sat, **params))
return
if ATTR_COLOR_TEMP in kwargs and (self._light_control.can_set_temp or
self._light_control.can_set_color):
temp = kwargs[ATTR_COLOR_TEMP]
if temp > self.max_mireds:
temp = self.max_mireds
elif temp < self.min_mireds:
temp = self.min_mireds
if brightness is None:
params[ATTR_TRANSITION_TIME] = transition_time
# White Spectrum bulb
if (self._light_control.can_set_temp and
not self._light_control.can_set_color):
await self._api(
self._light_control.set_color_temp(temp, **params))
# Color bulb (CWS)
# color_temp needs to be set with hue/saturation
if self._light_control.can_set_color:
params[ATTR_BRIGHTNESS] = brightness
temp_k = color_util.color_temperature_mired_to_kelvin(temp)
hs_color = color_util.color_temperature_to_hs(temp_k)
hue = int(hs_color[0] * (65535 / 360))
sat = int(hs_color[1] * (65279 / 100))
await self._api(
self._light_control.set_hsb(hue, sat,
**params))
if brightness is not None:
params[ATTR_TRANSITION_TIME] = transition_time
await self._api(
self._light_control.set_dimmer(brightness,
**params))
else:
yield from self._api(
await self._api(
self._light_control.set_state(True))
@callback
def _async_start_observe(self, exc=None):
"""Start observation of light."""
# pylint: disable=import-error
from pytradfri.error import PyTradFriError
from pytradfri.error import PytradfriError
if exc:
_LOGGER.warning("Observation failed for %s", self._name,
exc_info=exc)
@ -296,7 +307,7 @@ class TradfriLight(Light):
err_callback=self._async_start_observe,
duration=0)
self.hass.async_add_job(self._api(cmd))
except PyTradFriError as err:
except PytradfriError as err:
_LOGGER.warning("Observation failed, trying again", exc_info=err)
self._async_start_observe()
@ -309,27 +320,15 @@ class TradfriLight(Light):
self._light_control = light.light_control
self._light_data = light.light_control.lights[0]
self._name = light.name
self._hs_color = None
self._features = SUPPORTED_FEATURES
if self._light.device_info.manufacturer == IKEA:
if self._light_control.can_set_kelvin:
self._features |= SUPPORT_COLOR_TEMP
if self._light_control.can_set_color:
self._features |= SUPPORT_COLOR
else:
if self._light_data.hex_color is not None:
self._features |= SUPPORT_COLOR
self._temp_supported = self._light.device_info.manufacturer \
in ALLOWED_TEMPERATURES
if light.light_control.can_set_color:
self._features |= SUPPORT_COLOR
if light.light_control.can_set_temp:
self._features |= SUPPORT_COLOR_TEMP
@callback
def _observe_update(self, tradfri_device):
"""Receive new state data for this light."""
self._refresh(tradfri_device)
rgb = color_util.rgb_hex_to_rgb_list(
self._light_data.hex_color_inferred
)
self._hs_color = color_util.color_RGB_to_hs(*rgb)
self.async_schedule_update_ha_state()

View File

@ -38,6 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
'philips.light.ceiling',
'philips.light.zyceiling',
'philips.light.bulb',
'philips.light.candle',
'philips.light.candle2']),
})
@ -149,7 +150,9 @@ async def async_setup_platform(hass, config, async_add_devices,
device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id)
devices.append(device)
hass.data[DATA_KEY][host] = device
elif model in ['philips.light.bulb', 'philips.light.candle2']:
elif model in ['philips.light.bulb',
'philips.light.candle',
'philips.light.candle2']:
from miio import PhilipsBulb
light = PhilipsBulb(host, token)
device = XiaomiPhilipsBulb(name, light, model, unique_id)

View File

@ -16,7 +16,7 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP,
ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH,
SUPPORT_EFFECT, Light, PLATFORM_SCHEMA)
SUPPORT_EFFECT, Light, PLATFORM_SCHEMA, ATTR_ENTITY_ID, DOMAIN)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
@ -30,7 +30,7 @@ DEFAULT_TRANSITION = 350
CONF_SAVE_ON_CHANGE = 'save_on_change'
CONF_MODE_MUSIC = 'use_music_mode'
DOMAIN = 'yeelight'
DATA_KEY = 'light.yeelight'
DEVICE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
@ -90,6 +90,13 @@ YEELIGHT_EFFECT_LIST = [
EFFECT_TWITTER,
EFFECT_STOP]
SERVICE_SET_MODE = 'yeelight_set_mode'
ATTR_MODE = 'mode'
YEELIGHT_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
def _cmd(func):
"""Define a wrapper to catch exceptions from the bulb."""
@ -106,6 +113,11 @@ def _cmd(func):
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Yeelight bulbs."""
from yeelight.enums import PowerMode
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
lights = []
if discovery_info is not None:
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname'])
@ -115,16 +127,44 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
discovery_info['properties']['mac'])
device = {'name': name, 'ipaddr': discovery_info['host']}
lights.append(YeelightLight(device, DEVICE_SCHEMA({})))
light = YeelightLight(device, DEVICE_SCHEMA({}))
lights.append(light)
hass.data[DATA_KEY][name] = light
else:
for ipaddr, device_config in config[CONF_DEVICES].items():
_LOGGER.debug("Adding configured %s", device_config[CONF_NAME])
name = device_config[CONF_NAME]
_LOGGER.debug("Adding configured %s", name)
device = {'name': device_config[CONF_NAME], 'ipaddr': ipaddr}
lights.append(YeelightLight(device, device_config))
device = {'name': name, 'ipaddr': ipaddr}
light = YeelightLight(device, device_config)
lights.append(light)
hass.data[DATA_KEY][name] = light
add_devices(lights, True)
def service_handler(service):
"""Dispatch service calls to target entities."""
params = {key: value for key, value in service.data.items()
if key != ATTR_ENTITY_ID}
entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids:
target_devices = [dev for dev in hass.data[DATA_KEY].values()
if dev.entity_id in entity_ids]
else:
target_devices = hass.data[DATA_KEY].values()
for target_device in target_devices:
if service.service == SERVICE_SET_MODE:
target_device.set_mode(**params)
service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend({
vol.Required(ATTR_MODE):
vol.In([mode.name.lower() for mode in PowerMode])
})
hass.services.register(
DOMAIN, SERVICE_SET_MODE, service_handler,
schema=service_schema_set_mode)
class YeelightLight(Light):
"""Representation of a Yeelight light."""
@ -444,3 +484,11 @@ class YeelightLight(Light):
self._bulb.turn_off(duration=duration)
except yeelight.BulbException as ex:
_LOGGER.error("Unable to turn the bulb off: %s", ex)
def set_mode(self, mode: str):
"""Set a power mode."""
import yeelight
try:
self._bulb.set_power_mode(yeelight.enums.PowerMode[mode.upper()])
except yeelight.BulbException as ex:
_LOGGER.error("Unable to set the power mode: %s", ex)

View File

@ -15,7 +15,7 @@ from homeassistant.components.light import (
from homeassistant.const import CONF_HOST
import homeassistant.util.color as color_util
REQUIREMENTS = ['yeelightsunflower==0.0.8']
REQUIREMENTS = ['yeelightsunflower==0.0.10']
_LOGGER = logging.getLogger(__name__)

View File

@ -37,7 +37,7 @@ class BMWLock(LockDevice):
self._account = account
self._vehicle = vehicle
self._attribute = attribute
self._name = '{} {}'.format(self._vehicle.modelName, self._attribute)
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
self._sensor_name = sensor_name
self._state = None
@ -59,7 +59,7 @@ class BMWLock(LockDevice):
"""Return the state attributes of the lock."""
vehicle_state = self._vehicle.state
return {
'car': self._vehicle.modelName,
'car': self._vehicle.name,
'door_lock_state': vehicle_state.door_lock_state.value
}
@ -70,7 +70,7 @@ class BMWLock(LockDevice):
def lock(self, **kwargs):
"""Lock the car."""
_LOGGER.debug("%s: locking doors", self._vehicle.modelName)
_LOGGER.debug("%s: locking doors", self._vehicle.name)
# Optimistic state set here because it takes some time before the
# update callback response
self._state = STATE_LOCKED
@ -79,7 +79,7 @@ class BMWLock(LockDevice):
def unlock(self, **kwargs):
"""Unlock the car."""
_LOGGER.debug("%s: unlocking doors", self._vehicle.modelName)
_LOGGER.debug("%s: unlocking doors", self._vehicle.name)
# Optimistic state set here because it takes some time before the
# update callback response
self._state = STATE_UNLOCKED
@ -88,13 +88,17 @@ class BMWLock(LockDevice):
def update(self):
"""Update state of the lock."""
_LOGGER.debug("%s: updating data for %s", self._vehicle.modelName,
from bimmer_connected.state import LockState
_LOGGER.debug("%s: updating data for %s", self._vehicle.name,
self._attribute)
vehicle_state = self._vehicle.state
# Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED
self._state = (STATE_LOCKED if vehicle_state.door_lock_state.value
in ('LOCKED', 'SECURED') else STATE_UNLOCKED)
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
self._state = STATE_LOCKED \
if vehicle_state.door_lock_state \
in [LockState.LOCKED, LockState.SECURED] \
else STATE_UNLOCKED
def update_callback(self):
"""Schedule a state update."""

View File

@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA)
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2018.03.10']
REQUIREMENTS = ['youtube_dl==2018.04.03']
_LOGGER = logging.getLogger(__name__)

View File

@ -83,7 +83,8 @@ ATTR_MEDIA_SHUFFLE = 'shuffle'
MEDIA_TYPE_MUSIC = 'music'
MEDIA_TYPE_TVSHOW = 'tvshow'
MEDIA_TYPE_VIDEO = 'movie'
MEDIA_TYPE_MOVIE = 'movie'
MEDIA_TYPE_VIDEO = 'video'
MEDIA_TYPE_EPISODE = 'episode'
MEDIA_TYPE_CHANNEL = 'channel'
MEDIA_TYPE_PLAYLIST = 'playlist'

View File

@ -18,7 +18,7 @@ from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (dispatcher_send,
async_dispatcher_connect)
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA)
@ -517,7 +517,7 @@ class CastDevice(MediaPlayerDevice):
elif self.media_status.media_is_tvshow:
return MEDIA_TYPE_TVSHOW
elif self.media_status.media_is_movie:
return MEDIA_TYPE_VIDEO
return MEDIA_TYPE_MOVIE
elif self.media_status.media_is_musictrack:
return MEDIA_TYPE_MUSIC
return None

View File

@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.components.media_player import (
MEDIA_TYPE_CHANNEL, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_EPISODE,
MEDIA_TYPE_VIDEO, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP,
MEDIA_TYPE_MOVIE, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP,
SUPPORT_VOLUME_MUTE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, DOMAIN, PLATFORM_SCHEMA,
MediaPlayerDevice)
@ -281,7 +281,7 @@ class ChannelsPlayer(MediaPlayerDevice):
if media_type == MEDIA_TYPE_CHANNEL:
response = self.client.play_channel(media_id)
self.update_state(response)
elif media_type in [MEDIA_TYPE_VIDEO, MEDIA_TYPE_EPISODE,
elif media_type in [MEDIA_TYPE_MOVIE, MEDIA_TYPE_EPISODE,
MEDIA_TYPE_TVSHOW]:
response = self.client.play_recording(media_id)
self.update_state(response)

View File

@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PLAY,
@ -147,7 +147,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
@property
def media_content_type(self):
"""Return the content type of current playing media."""
return MEDIA_TYPE_VIDEO
return MEDIA_TYPE_MOVIE
@property
def media_duration(self):

View File

@ -8,7 +8,7 @@ import voluptuous as vol
import requests
from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA,
SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY,
MediaPlayerDevice)
@ -154,7 +154,7 @@ class DirecTvDevice(MediaPlayerDevice):
"""Return the content type of current playing media."""
if 'episodeTitle' in self._current:
return MEDIA_TYPE_TVSHOW
return MEDIA_TYPE_VIDEO
return MEDIA_TYPE_MOVIE
@property
def media_channel(self):

View File

@ -10,7 +10,7 @@ import logging
import voluptuous as vol
from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA)
from homeassistant.const import (
@ -231,7 +231,7 @@ class EmbyDevice(MediaPlayerDevice):
if media_type == 'Episode':
return MEDIA_TYPE_TVSHOW
elif media_type == 'Movie':
return MEDIA_TYPE_VIDEO
return MEDIA_TYPE_MOVIE
elif media_type == 'Trailer':
return MEDIA_TYPE_TRAILER
elif media_type == 'Music':

View File

@ -19,8 +19,8 @@ from homeassistant.components.media_player import (
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET,
MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_PLAYLIST,
MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON)
MEDIA_TYPE_MOVIE, MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL,
MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON)
from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
CONF_PORT, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD,
@ -67,7 +67,7 @@ MEDIA_TYPES = {
'video': MEDIA_TYPE_VIDEO,
'set': MEDIA_TYPE_PLAYLIST,
'musicvideo': MEDIA_TYPE_VIDEO,
'movie': MEDIA_TYPE_VIDEO,
'movie': MEDIA_TYPE_MOVIE,
'tvshow': MEDIA_TYPE_TVSHOW,
'season': MEDIA_TYPE_TVSHOW,
'episode': MEDIA_TYPE_TVSHOW,

View File

@ -22,7 +22,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.3']
REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.4']
_LOGGER = logging.getLogger(__name__)

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
from homeassistant.helpers.script import Script
from homeassistant.util import Throttle
REQUIREMENTS = ['ha-philipsjs==0.0.2']
REQUIREMENTS = ['ha-philipsjs==0.0.3']
_LOGGER = logging.getLogger(__name__)

View File

@ -14,7 +14,7 @@ import voluptuous as vol
from homeassistant import util
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, PLATFORM_SCHEMA,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, PLATFORM_SCHEMA,
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK,
SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
@ -480,7 +480,7 @@ class PlexClient(MediaPlayerDevice):
self._media_episode = str(self._session.index).zfill(2)
elif self._session_type == 'movie':
self._media_content_type = MEDIA_TYPE_VIDEO
self._media_content_type = MEDIA_TYPE_MOVIE
if self._session.year is not None and \
self._media_title is not None:
self._media_title += ' (' + str(self._session.year) + ')'
@ -576,7 +576,7 @@ class PlexClient(MediaPlayerDevice):
elif self._session_type == 'episode':
return MEDIA_TYPE_TVSHOW
elif self._session_type == 'movie':
return MEDIA_TYPE_VIDEO
return MEDIA_TYPE_MOVIE
elif self._session_type == 'track':
return MEDIA_TYPE_MUSIC

View File

@ -9,7 +9,7 @@ import logging
import voluptuous as vol
from homeassistant.components.media_player import (
MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA,
MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.const import (
@ -155,7 +155,7 @@ class RokuDevice(MediaPlayerDevice):
return None
elif self.current_app.name == "Roku":
return None
return MEDIA_TYPE_VIDEO
return MEDIA_TYPE_MOVIE
@property
def media_image_url(self):

View File

@ -17,7 +17,7 @@ from homeassistant.const import (
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-songpal==0.0.6']
REQUIREMENTS = ['python-songpal==0.0.7']
SUPPORT_SONGPAL = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \
@ -101,7 +101,7 @@ class SongpalDevice(MediaPlayerDevice):
import songpal
self._name = name
self.endpoint = endpoint
self.dev = songpal.Protocol(self.endpoint)
self.dev = songpal.Device(self.endpoint)
self._sysinfo = None
self._state = False

View File

@ -35,6 +35,7 @@ CONF_SOURCES = 'sources'
CONF_ON_ACTION = 'turn_on_action'
DEFAULT_NAME = 'LG webOS Smart TV'
LIVETV_APP_ID = 'com.webos.app.livetv'
WEBOSTV_CONFIG_FILE = 'webostv.conf'
@ -357,8 +358,16 @@ class LgWebOSDevice(MediaPlayerDevice):
def media_next_track(self):
"""Send next track command."""
self._client.fast_forward()
current_input = self._client.get_input()
if current_input == LIVETV_APP_ID:
self._client.channel_up()
else:
self._client.fast_forward()
def media_previous_track(self):
"""Send the previous track command."""
self._client.rewind()
current_input = self._client.get_input()
if current_input == LIVETV_APP_ID:
self._client.channel_down()
else:
self._client.rewind()

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