1
mirror of https://github.com/home-assistant/core synced 2024-07-21 14:24:50 +02:00

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/*/maxcube.py homeassistant/components/*/maxcube.py
homeassistant/components/mercedesme.py
homeassistant/components/*/mercedesme.py
homeassistant/components/mochad.py homeassistant/components/mochad.py
homeassistant/components/*/mochad.py homeassistant/components/*/mochad.py
@ -289,11 +286,9 @@ omit =
homeassistant/components/*/wink.py homeassistant/components/*/wink.py
homeassistant/components/xiaomi_aqara.py homeassistant/components/xiaomi_aqara.py
homeassistant/components/binary_sensor/xiaomi_aqara.py homeassistant/components/*/xiaomi_aqara.py
homeassistant/components/cover/xiaomi_aqara.py
homeassistant/components/light/xiaomi_aqara.py homeassistant/components/*/xiaomi_miio.py
homeassistant/components/sensor/xiaomi_aqara.py
homeassistant/components/switch/xiaomi_aqara.py
homeassistant/components/zabbix.py homeassistant/components/zabbix.py
homeassistant/components/*/zabbix.py homeassistant/components/*/zabbix.py
@ -357,6 +352,7 @@ omit =
homeassistant/components/climate/touchline.py homeassistant/components/climate/touchline.py
homeassistant/components/climate/venstar.py homeassistant/components/climate/venstar.py
homeassistant/components/cover/garadget.py homeassistant/components/cover/garadget.py
homeassistant/components/cover/gogogate2.py
homeassistant/components/cover/homematic.py homeassistant/components/cover/homematic.py
homeassistant/components/cover/knx.py homeassistant/components/cover/knx.py
homeassistant/components/cover/myq.py homeassistant/components/cover/myq.py
@ -374,6 +370,7 @@ omit =
homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/google_maps.py
homeassistant/components/device_tracker/gpslogger.py homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/hitron_coda.py homeassistant/components/device_tracker/hitron_coda.py
homeassistant/components/device_tracker/huawei_router.py homeassistant/components/device_tracker/huawei_router.py
@ -400,8 +397,8 @@ omit =
homeassistant/components/emoncms_history.py homeassistant/components/emoncms_history.py
homeassistant/components/emulated_hue/upnp.py homeassistant/components/emulated_hue/upnp.py
homeassistant/components/fan/mqtt.py homeassistant/components/fan/mqtt.py
homeassistant/components/fan/xiaomi_miio.py
homeassistant/components/feedreader.py homeassistant/components/feedreader.py
homeassistant/components/folder_watcher.py
homeassistant/components/foursquare.py homeassistant/components/foursquare.py
homeassistant/components/goalfeed.py homeassistant/components/goalfeed.py
homeassistant/components/ifttt.py homeassistant/components/ifttt.py
@ -424,6 +421,7 @@ omit =
homeassistant/components/light/lifx.py homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py homeassistant/components/light/limitlessled.py
homeassistant/components/light/mystrom.py homeassistant/components/light/mystrom.py
homeassistant/components/light/nanoleaf_aurora.py
homeassistant/components/light/osramlightify.py homeassistant/components/light/osramlightify.py
homeassistant/components/light/piglow.py homeassistant/components/light/piglow.py
homeassistant/components/light/rpi_gpio_pwm.py homeassistant/components/light/rpi_gpio_pwm.py
@ -432,7 +430,6 @@ omit =
homeassistant/components/light/tplink.py homeassistant/components/light/tplink.py
homeassistant/components/light/tradfri.py homeassistant/components/light/tradfri.py
homeassistant/components/light/x10.py homeassistant/components/light/x10.py
homeassistant/components/light/xiaomi_miio.py
homeassistant/components/light/yeelight.py homeassistant/components/light/yeelight.py
homeassistant/components/light/yeelightsunflower.py homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/zengge.py homeassistant/components/light/zengge.py
@ -441,6 +438,7 @@ omit =
homeassistant/components/lock/nello.py homeassistant/components/lock/nello.py
homeassistant/components/lock/nuki.py homeassistant/components/lock/nuki.py
homeassistant/components/lock/sesame.py homeassistant/components/lock/sesame.py
homeassistant/components/map.py
homeassistant/components/media_extractor.py homeassistant/components/media_extractor.py
homeassistant/components/media_player/anthemav.py homeassistant/components/media_player/anthemav.py
homeassistant/components/media_player/aquostv.py homeassistant/components/media_player/aquostv.py
@ -508,6 +506,7 @@ omit =
homeassistant/components/notify/kodi.py homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/llamalab_automate.py
homeassistant/components/notify/mastodon.py
homeassistant/components/notify/matrix.py homeassistant/components/notify/matrix.py
homeassistant/components/notify/message_bird.py homeassistant/components/notify/message_bird.py
homeassistant/components/notify/mycroft.py homeassistant/components/notify/mycroft.py
@ -523,8 +522,8 @@ omit =
homeassistant/components/notify/sendgrid.py homeassistant/components/notify/sendgrid.py
homeassistant/components/notify/simplepush.py homeassistant/components/notify/simplepush.py
homeassistant/components/notify/slack.py homeassistant/components/notify/slack.py
homeassistant/components/notify/stride.py
homeassistant/components/notify/smtp.py homeassistant/components/notify/smtp.py
homeassistant/components/notify/stride.py
homeassistant/components/notify/synology_chat.py homeassistant/components/notify/synology_chat.py
homeassistant/components/notify/syslog.py homeassistant/components/notify/syslog.py
homeassistant/components/notify/telegram.py homeassistant/components/notify/telegram.py
@ -538,7 +537,6 @@ omit =
homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remember_the_milk/__init__.py
homeassistant/components/remote/harmony.py homeassistant/components/remote/harmony.py
homeassistant/components/remote/itach.py homeassistant/components/remote/itach.py
homeassistant/components/remote/xiaomi_miio.py
homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/scene/lifx_cloud.py homeassistant/components/scene/lifx_cloud.py
homeassistant/components/sensor/airvisual.py homeassistant/components/sensor/airvisual.py
@ -674,6 +672,7 @@ omit =
homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/viaggiatreno.py homeassistant/components/sensor/viaggiatreno.py
homeassistant/components/sensor/waqi.py homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/waze_travel_time.py
homeassistant/components/sensor/whois.py homeassistant/components/sensor/whois.py
homeassistant/components/sensor/worldtidesinfo.py homeassistant/components/sensor/worldtidesinfo.py
homeassistant/components/sensor/worxlandroid.py homeassistant/components/sensor/worxlandroid.py
@ -707,7 +706,6 @@ omit =
homeassistant/components/switch/tplink.py homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py homeassistant/components/switch/transmission.py
homeassistant/components/switch/vesync.py homeassistant/components/switch/vesync.py
homeassistant/components/switch/xiaomi_miio.py
homeassistant/components/telegram_bot/* homeassistant/components/telegram_bot/*
homeassistant/components/thingspeak.py homeassistant/components/thingspeak.py
homeassistant/components/tts/amazon_polly.py homeassistant/components/tts/amazon_polly.py
@ -716,7 +714,6 @@ omit =
homeassistant/components/tts/picotts.py homeassistant/components/tts/picotts.py
homeassistant/components/vacuum/mqtt.py homeassistant/components/vacuum/mqtt.py
homeassistant/components/vacuum/roomba.py homeassistant/components/vacuum/roomba.py
homeassistant/components/vacuum/xiaomi_miio.py
homeassistant/components/weather/bom.py homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.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 with the issue:**
<!--
**Home Assistant release (`hass --version`):** - 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:** **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:** **Description of problem:**
**Expected:**
**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):**
**Problem-relevant `configuration.yaml` entries and steps to reproduce:**
```yaml ```yaml
``` ```
1.
2.
3.
**Traceback (if applicable):** **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/websocket_api.py @home-assistant/core
homeassistant/components/zone.py @home-assistant/core homeassistant/components/zone.py @home-assistant/core
# To monitor non-pypi additions
requirements_all.txt @andrey-git
# HomeAssistant developer Teams # HomeAssistant developer Teams
Dockerfile @home-assistant/docker Dockerfile @home-assistant/docker
virtualization/Docker/* @home-assistant/docker virtualization/Docker/* @home-assistant/docker
@ -43,6 +40,7 @@ homeassistant/components/hassio.py @home-assistant/hassio
# Individual components # Individual components
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt 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/binary_sensor/hikvision.py @mezz64
homeassistant/components/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/camera/yi.py @bachya 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/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/pollen.py @bachya 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/sql.py @dgomes
homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya homeassistant/components/switch/rainmachine.py @bachya
@ -80,17 +80,17 @@ homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/axis.py @kane610 homeassistant/components/*/axis.py @kane610
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/*/deconz.py @kane610
homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/eight_sleep.py @mezz64
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/*/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p 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/*/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/*/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei
homeassistant/components/tesla.py @zabuldon 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/*/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tradfri.py @ggravlingen 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_aqara.py @danielhiversen @syssi
homeassistant/components/*/xiaomi_miio.py @rytilahti @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 import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['abodepy==0.12.2'] REQUIREMENTS = ['abodepy==0.12.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -93,7 +93,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class IFTTTAlarmPanel(alarm.AlarmControlPanel): 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, def __init__(self, name, code, event_away, event_home, event_night,
event_disarm, optimistic): event_disarm, optimistic):

View File

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

View File

@ -6,18 +6,20 @@ from datetime import datetime
from uuid import uuid4 from uuid import uuid4
from homeassistant.components import ( 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) media_player, scene, script, switch, http, sensor)
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS, SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON) CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
from .const import CONF_FILTER, CONF_ENTITY_CONFIG from .const import CONF_FILTER, CONF_ENTITY_CONFIG
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -34,6 +36,16 @@ API_TEMP_UNITS = {
TEMP_CELSIUS: 'CELSIUS', 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' SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
CONF_DESCRIPTION = 'description' CONF_DESCRIPTION = 'description'
@ -383,8 +395,60 @@ class _AlexaTemperatureSensor(_AlexaInterface):
raise _UnsupportedProperty(name) raise _UnsupportedProperty(name)
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT] 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 { 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], 'scale': API_TEMP_UNITS[unit],
} }
@ -415,6 +479,16 @@ class _SwitchCapabilities(_AlexaEntity):
return [_AlexaPowerController(self.entity)] 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) @ENTITY_ADAPTERS.register(cover.DOMAIN)
class _CoverCapabilities(_AlexaEntity): class _CoverCapabilities(_AlexaEntity):
def default_display_categories(self): def default_display_categories(self):
@ -682,17 +756,26 @@ def api_message(request,
return response 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. """Create a API formatted error response.
Async friendly. Async friendly.
""" """
payload = { payload = payload or {}
'type': error_type, payload['type'] = error_type
'message': error_message, 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')) @HANDLERS.register(('Alexa.Discovery', 'Discover'))
@ -1104,7 +1187,6 @@ def async_api_select_input(hass, config, request, entity):
else: else:
msg = 'failed to map input {} to a media source on {}'.format( msg = 'failed to map input {} to a media source on {}'.format(
media_input, entity.entity_id) media_input, entity.entity_id)
_LOGGER.error(msg)
return api_error( return api_error(
request, error_type='INVALID_VALUE', error_message=msg) request, error_type='INVALID_VALUE', error_message=msg)
@ -1276,6 +1358,149 @@ def async_api_previous(hass, config, request, entity):
return api_message(request) 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')) @HANDLERS.register(('Alexa', 'ReportState'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine

View File

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

View File

@ -52,9 +52,8 @@ def setup(hass, config):
hass.http.register_view(APIComponentsView) hass.http.register_view(APIComponentsView)
hass.http.register_view(APITemplateView) hass.http.register_view(APITemplateView)
log_path = hass.data.get(DATA_LOGGING, None) if DATA_LOGGING in hass.data:
if log_path: hass.http.register_view(APIErrorLog)
hass.http.register_static_path(URL_API_ERROR_LOG, log_path, False)
return True return True
@ -356,6 +355,17 @@ class APITemplateView(HomeAssistantView):
HTTP_BAD_REQUEST) 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 @asyncio.coroutine
def async_services_json(hass): def async_services_json(hass):
"""Generate services data to JSONify.""" """Generate services data to JSONify."""

View File

@ -7,8 +7,8 @@ https://home-assistant.io/components/binary_sensor.bmw_connected_drive/
import asyncio import asyncio
import logging import logging
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
DEPENDENCIES = ['bmw_connected_drive'] DEPENDENCIES = ['bmw_connected_drive']
@ -45,7 +45,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
self._account = account self._account = account
self._vehicle = vehicle self._vehicle = vehicle
self._attribute = attribute 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._sensor_name = sensor_name
self._device_class = device_class self._device_class = device_class
self._state = None self._state = None
@ -75,7 +75,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
"""Return the state attributes of the binary sensor.""" """Return the state attributes of the binary sensor."""
vehicle_state = self._vehicle.state vehicle_state = self._vehicle.state
result = { result = {
'car': self._vehicle.modelName 'car': self._vehicle.name
} }
if self._attribute == 'lids': if self._attribute == 'lids':
@ -91,6 +91,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
def update(self): def update(self):
"""Read new state data from the library.""" """Read new state data from the library."""
from bimmer_connected.state import LockState
vehicle_state = self._vehicle.state vehicle_state = self._vehicle.state
# device class opening: On means open, Off means closed # device class opening: On means open, Off means closed
@ -101,9 +102,9 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
self._state = not vehicle_state.all_windows_closed self._state = not vehicle_state.all_windows_closed
# device class safety: On means unsafe, Off means safe # device class safety: On means unsafe, Off means safe
if self._attribute == 'door_lock_state': if self._attribute == 'door_lock_state':
# Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
self._state = bool(vehicle_state.door_lock_state.value self._state = vehicle_state.door_lock_state not in \
in ('SELECTIVELOCKED', 'UNLOCKED')) [LockState.LOCKED, LockState.SECURED]
def update_callback(self): def update_callback(self):
"""Schedule a state update.""" """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): async def async_setup_platform(
"""Set up the MySensors platform for binary sensors.""" hass, config, async_add_devices, discovery_info=None):
"""Set up the mysensors platform for binary sensors."""
mysensors.setup_mysensors_platform( mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsBinarySensor, hass, DOMAIN, discovery_info, MySensorsBinarySensor,
add_devices=add_devices) async_add_devices=async_add_devices)
class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice): 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', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES', 'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
'Sweden', 'SE', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK',
'Wales'] 'UnitedStates', 'US', 'Wales']
CONF_COUNTRY = 'country' CONF_COUNTRY = 'country'
CONF_PROVINCE = 'province' CONF_PROVINCE = 'province'
CONF_WORKDAYS = 'workdays' CONF_WORKDAYS = 'workdays'
@ -47,13 +47,13 @@ DEFAULT_OFFSET = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES), 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_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int), vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int),
vol.Optional(CONF_PROVINCE): cv.string,
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS): vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]), 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: if province:
# 'state' and 'prov' are not interchangeable, so need to make # 'state' and 'prov' are not interchangeable, so need to make
# sure we use the right one # sure we use the right one
if (hasattr(obj_holidays, "PROVINCES") and if (hasattr(obj_holidays, 'PROVINCES') and
province in obj_holidays.PROVINCES): province in obj_holidays.PROVINCES):
obj_holidays = getattr(holidays, country)(prov=province, obj_holidays = getattr(holidays, country)(
years=year) prov=province, years=year)
elif (hasattr(obj_holidays, "STATES") and elif (hasattr(obj_holidays, 'STATES') and
province in obj_holidays.STATES): province in obj_holidays.STATES):
obj_holidays = getattr(holidays, country)(state=province, obj_holidays = getattr(holidays, country)(
years=year) state=province, years=year)
else: else:
_LOGGER.error("There is no province/state %s in country %s", _LOGGER.error("There is no province/state %s in country %s",
province, country) 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/bmw_connected_drive/ https://home-assistant.io/components/bmw_connected_drive/
""" """
import logging
import datetime import datetime
import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv 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__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'bmw_connected_drive' DOMAIN = 'bmw_connected_drive'
CONF_VALUES = 'values' CONF_REGION = 'region'
CONF_COUNTRY = 'country'
ACCOUNT_SCHEMA = vol.Schema({ ACCOUNT_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): 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({ CONFIG_SCHEMA = vol.Schema({
@ -47,9 +46,9 @@ def setup(hass, config):
for name, account_config in config[DOMAIN].items(): for name, account_config in config[DOMAIN].items():
username = account_config[CONF_USERNAME] username = account_config[CONF_USERNAME]
password = account_config[CONF_PASSWORD] password = account_config[CONF_PASSWORD]
country = account_config[CONF_COUNTRY] region = account_config[CONF_REGION]
_LOGGER.debug('Adding new account %s', name) _LOGGER.debug('Adding new account %s', name)
bimmer = BMWConnectedDriveAccount(username, password, country, name) bimmer = BMWConnectedDriveAccount(username, password, region, name)
accounts.append(bimmer) accounts.append(bimmer)
# update every UPDATE_INTERVAL minutes, starting now # update every UPDATE_INTERVAL minutes, starting now
@ -75,12 +74,15 @@ def setup(hass, config):
class BMWConnectedDriveAccount(object): class BMWConnectedDriveAccount(object):
"""Representation of a BMW vehicle.""" """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: name: str) -> None:
"""Constructor.""" """Constructor."""
from bimmer_connected.account import ConnectedDriveAccount 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.name = name
self._update_listeners = [] self._update_listeners = []

View File

@ -19,7 +19,6 @@ from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_TOPIC = 'topic' CONF_TOPIC = 'topic'
DEFAULT_NAME = 'MQTT Camera' DEFAULT_NAME = 'MQTT Camera'
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
@ -33,9 +32,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the MQTT Camera.""" """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): 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.helpers import discovery
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['py-canary==0.4.1'] REQUIREMENTS = ['py-canary==0.5.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

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

View File

@ -179,7 +179,7 @@ class NestThermostat(ClimateDevice):
try: try:
self.device.target = temp self.device.target = temp
except nest.nest.APIError: 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): def set_operation_mode(self, operation_mode):
"""Set operation mode.""" """Set operation mode."""

View File

@ -14,24 +14,16 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config' DOMAIN = 'config'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script', SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
'entity_registry') 'entity_registry', 'config_entries')
ON_DEMAND = ('zwave',) ON_DEMAND = ('zwave',)
FEATURE_FLAGS = ('config_entries',)
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Set up the config component.""" """Set up the config component."""
global SECTIONS
yield from hass.components.frontend.async_register_built_in_panel( yield from hass.components.frontend.async_register_built_in_panel(
'config', 'config', 'mdi:settings') '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 @asyncio.coroutine
def setup_panel(panel_name): def setup_panel(panel_name):
"""Set up a panel.""" """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 import http
from homeassistant.components.http.data_validator import ( from homeassistant.components.http.data_validator import (
RequestDataValidator) 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 config_validation as cv
from homeassistant.helpers import intent from homeassistant.helpers import intent
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.setup import (ATTR_COMPONENT)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -28,6 +32,13 @@ DOMAIN = 'conversation'
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)') REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REGEX_TYPE = type(re.compile('')) 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 = 'process'
SERVICE_PROCESS_SCHEMA = vol.Schema({ SERVICE_PROCESS_SCHEMA = vol.Schema({
@ -112,6 +123,25 @@ async def async_setup(hass, config):
'[the] [a] [an] {name}[s] toggle', '[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 return True

View File

@ -17,6 +17,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components import group from homeassistant.components import group
from homeassistant.helpers import intent
from homeassistant.const import ( from homeassistant.const import (
SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT, SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT,
@ -55,6 +56,9 @@ ATTR_CURRENT_TILT_POSITION = 'current_tilt_position'
ATTR_POSITION = 'position' ATTR_POSITION = 'position'
ATTR_TILT_POSITION = 'tilt_position' ATTR_TILT_POSITION = 'tilt_position'
INTENT_OPEN_COVER = 'HassOpenCover'
INTENT_CLOSE_COVER = 'HassCloseCover'
COVER_SERVICE_SCHEMA = vol.Schema({ COVER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
}) })
@ -181,6 +185,12 @@ async def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
DOMAIN, service_name, async_handle_cover_service, DOMAIN, service_name, async_handle_cover_service,
schema=schema) 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 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 from homeassistant.const import STATE_OFF, STATE_ON
def setup_platform(hass, config, add_devices, discovery_info=None): async def async_setup_platform(
"""Set up the MySensors platform for covers.""" hass, config, async_add_devices, discovery_info=None):
"""Set up the mysensors platform for covers."""
mysensors.setup_mysensors_platform( 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): class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):

View File

@ -87,7 +87,7 @@ class RPiGPIOCover(CoverDevice):
self._invert_relay = invert_relay self._invert_relay = invert_relay
rpi_gpio.setup_output(self._relay_pin) rpi_gpio.setup_output(self._relay_pin)
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode) 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 @property
def name(self): def name(self):
@ -105,9 +105,9 @@ class RPiGPIOCover(CoverDevice):
def _trigger(self): def _trigger(self):
"""Trigger the cover.""" """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) 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): def close_cover(self, **kwargs):
"""Close the cover.""" """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 import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.discovery import SERVICE_DECONZ from homeassistant.components.discovery import SERVICE_DECONZ
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv 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.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.json import load_json, save_json from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['pydeconz==32'] REQUIREMENTS = ['pydeconz==35']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -160,7 +161,8 @@ async def async_request_configuration(hass, config, deconz_config):
async def async_configuration_callback(data): async def async_configuration_callback(data):
"""Set up actions to do when our configuration callback is called.""" """Set up actions to do when our configuration callback is called."""
from pydeconz.utils import async_get_api_key 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: if api_key:
deconz_config[CONF_API_KEY] = api_key deconz_config[CONF_API_KEY] = api_key
result = await async_setup_deconz(hass, config, deconz_config) 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", entity_picture="/static/images/logo_deconz.jpeg",
submit_caption="I have unlocked the gateway", 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 import logging
from typing import Any, List, Sequence, Callable from typing import Any, List, Sequence, Callable
import aiohttp
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.setup import async_prepare_setup_platform 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.components import group, zone
from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError 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 import config_per_platform, discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
@ -76,7 +73,6 @@ ATTR_LOCATION_NAME = 'location_name'
ATTR_MAC = 'mac' ATTR_MAC = 'mac'
ATTR_NAME = 'name' ATTR_NAME = 'name'
ATTR_SOURCE_TYPE = 'source_type' ATTR_SOURCE_TYPE = 'source_type'
ATTR_VENDOR = 'vendor'
ATTR_CONSIDER_HOME = 'consider_home' ATTR_CONSIDER_HOME = 'consider_home'
SOURCE_TYPE_GPS = 'gps' SOURCE_TYPE_GPS = 'gps'
@ -328,14 +324,10 @@ class DeviceTracker(object):
self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False,
name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id]) 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, { self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
ATTR_ENTITY_ID: device.entity_id, ATTR_ENTITY_ID: device.entity_id,
ATTR_HOST_NAME: device.host_name, ATTR_HOST_NAME: device.host_name,
ATTR_MAC: device.mac, ATTR_MAC: device.mac,
ATTR_VENDOR: device.vendor,
}) })
# update known_devices.yaml # update known_devices.yaml
@ -413,7 +405,6 @@ class Device(Entity):
consider_home = None # type: dt_util.dt.timedelta consider_home = None # type: dt_util.dt.timedelta
battery = None # type: int battery = None # type: int
attributes = None # type: dict attributes = None # type: dict
vendor = None # type: str
icon = None # type: str icon = None # type: str
# Track if the last update of this device was HOME. # 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, def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str = None, track: bool, dev_id: str, mac: str, name: str = None,
picture: str = None, gravatar: str = None, icon: 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.""" """Initialize a device."""
self.hass = hass self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id) self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@ -451,7 +442,6 @@ class Device(Entity):
self.icon = icon self.icon = icon
self.away_hide = hide_if_away self.away_hide = hide_if_away
self.vendor = vendor
self.source_type = None self.source_type = None
@ -567,51 +557,6 @@ class Device(Entity):
self._state = STATE_HOME self._state = STATE_HOME
self.last_update_home = True 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 @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Add an entity.""" """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('picture', default=None): vol.Any(None, cv.string),
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
cv.time_period, cv.positive_timedelta), cv.time_period, cv.positive_timedelta),
vol.Optional('vendor', default=None): vol.Any(None, cv.string),
}) })
try: try:
result = [] result = []
@ -697,6 +641,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
return [] return []
for dev_id, device in devices.items(): for dev_id, device in devices.items():
# Deprecated option. We just ignore it to avoid breaking change
device.pop('vendor', None)
try: try:
device = dev_schema(device) device = dev_schema(device)
device['dev_id'] = cv.slugify(dev_id) 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, 'picture': device.config_picture,
'track': device.track, 'track': device.track,
CONF_AWAY_HIDE: device.away_hide, CONF_AWAY_HIDE: device.away_hide,
'vendor': device.vendor,
}} }}
out.write('\n') out.write('\n')
out.write(dump(device)) out.write(dump(device))

View File

@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_PUB_KEY = 'pub_key' CONF_PUB_KEY = 'pub_key'
CONF_SSH_KEY = 'ssh_key' CONF_SSH_KEY = 'ssh_key'
CONF_REQUIRE_IP = 'require_ip'
DEFAULT_SSH_PORT = 22 DEFAULT_SSH_PORT = 22
SECRET_GROUP = 'Password or SSH Key' 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_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']),
vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']), vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']),
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, 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_PASSWORD, SECRET_GROUP): cv.string,
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
vol.Exclusive(CONF_PUB_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.protocol = config[CONF_PROTOCOL]
self.mode = config[CONF_MODE] self.mode = config[CONF_MODE]
self.port = config[CONF_PORT] self.port = config[CONF_PORT]
self.require_ip = config[CONF_REQUIRE_IP]
if self.protocol == 'ssh': if self.protocol == 'ssh':
self.connection = SshConnection( self.connection = SshConnection(
@ -172,7 +175,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
ret_devices = {} ret_devices = {}
for key in 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] ret_devices[key] = devices[key]
return ret_devices return ret_devices

View File

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

View File

@ -36,16 +36,20 @@ class BMWDeviceTracker(object):
self.vehicle = vehicle self.vehicle = vehicle
def update(self) -> None: def update(self) -> None:
"""Update the device info.""" """Update the device info.
dev_id = slugify(self.vehicle.modelName)
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) _LOGGER.debug('Updating %s', dev_id)
attrs = {
'trackr_id': dev_id,
'id': dev_id,
'name': self.vehicle.modelName
}
self._see( self._see(
dev_id=dev_id, host_name=self.vehicle.modelName, dev_id=dev_id, host_name=self.vehicle.name,
gps=self.vehicle.state.gps_position, attributes=attrs, gps=self.vehicle.state.gps_position, icon='mdi:car'
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 import mysensors
from homeassistant.components.device_tracker import DOMAIN 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 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.""" """Set up the MySensors device scanner."""
new_devices = mysensors.setup_mysensors_platform( new_devices = mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsDeviceScanner, hass, DOMAIN, discovery_info, MySensorsDeviceScanner,
device_args=(see, )) device_args=(async_see, ))
if not new_devices: if not new_devices:
return False return False
@ -22,9 +22,9 @@ def setup_scanner(hass, config, see, discovery_info=None):
dev_id = ( dev_id = (
id(device.gateway), device.node_id, device.child_id, id(device.gateway), device.node_id, device.child_id,
device.value_type) device.value_type)
dispatcher_connect( async_dispatcher_connect(
hass, mysensors.SIGNAL_CALLBACK.format(*dev_id), hass, mysensors.SIGNAL_CALLBACK.format(*dev_id),
device.update_callback) device.async_update_callback)
return True return True
@ -32,20 +32,20 @@ def setup_scanner(hass, config, see, discovery_info=None):
class MySensorsDeviceScanner(mysensors.MySensorsDevice): class MySensorsDeviceScanner(mysensors.MySensorsDevice):
"""Represent a MySensors scanner.""" """Represent a MySensors scanner."""
def __init__(self, see, *args): def __init__(self, async_see, *args):
"""Set up instance.""" """Set up instance."""
super().__init__(*args) super().__init__(*args)
self.see = see self.async_see = async_see
def update_callback(self): async def async_update_callback(self):
"""Update the device.""" """Update the device."""
self.update() await self.async_update()
node = self.gateway.sensors[self.node_id] node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id] child = node.children[self.child_id]
position = child.values[self.value_type] position = child.values[self.value_type]
latitude, longitude, _ = position.split(',') latitude, longitude, _ = position.split(',')
self.see( await self.async_see(
dev_id=slugify(self.name), dev_id=slugify(self.name),
host_name=self.name, host_name=self.name,
gps=(latitude, longitude), gps=(latitude, longitude),

View File

@ -95,7 +95,7 @@ class UbusDeviceScanner(DeviceScanner):
return self.last_results return self.last_results
def _generate_mac2name(self): 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() self.mac2name = dict()
@_refresh_on_access_denied @_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 import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_HOMEASSISTANT_START
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -40,6 +41,10 @@ SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin' SERVICE_DAIKIN = 'daikin'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
CONFIG_ENTRY_HANDLERS = {
SERVICE_HUE: 'hue',
}
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_HASS_IOS_APP: ('ios', None),
SERVICE_NETGEAR: ('device_tracker', None), SERVICE_NETGEAR: ('device_tracker', None),
@ -51,7 +56,6 @@ SERVICE_HANDLERS = {
SERVICE_WINK: ('wink', None), SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_TELLDUSLIVE: ('tellduslive', None),
SERVICE_HUE: ('hue', None),
SERVICE_DECONZ: ('deconz', None), SERVICE_DECONZ: ('deconz', None),
SERVICE_DAIKIN: ('daikin', None), SERVICE_DAIKIN: ('daikin', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
@ -105,6 +109,20 @@ async def async_setup(hass, config):
logger.info("Ignoring service: %s %s", service, info) logger.info("Ignoring service: %s %s", service, info)
return 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) comp_plat = SERVICE_HANDLERS.get(service)
# We do not know how to handle this 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) logger.info("Unknown service discovered: %s %s", service, info)
return 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) logger.info("Found new service: %s %s", service, info)
component, platform = comp_plat component, platform = comp_plat

View File

@ -22,6 +22,7 @@ DOMAIN = 'doorbird'
API_URL = '/api/{}'.format(DOMAIN) API_URL = '/api/{}'.format(DOMAIN)
CONF_DOORBELL_EVENTS = 'doorbell_events' CONF_DOORBELL_EVENTS = 'doorbell_events'
CONF_CUSTOM_URL = 'hass_url_override'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -29,6 +30,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean, vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
vol.Optional(CONF_CUSTOM_URL): cv.string,
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -61,9 +63,17 @@ def setup(hass, config):
# Provide an endpoint for the device to call to trigger events # Provide an endpoint for the device to call to trigger events
hass.http.register_view(DoorbirdRequestView()) 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 # This will make HA the only service that gets doorbell events
url = '{}{}/{}'.format( url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL)
hass.config.api.base_url, API_URL, SENSOR_DOORBELL)
device.reset_notifications() device.reset_notifications()
device.subscribe_notification(SENSOR_DOORBELL, url) device.subscribe_notification(SENSOR_DOORBELL, url)

View File

@ -158,10 +158,6 @@ class Config(object):
"Listen port not specified, defaulting to %s", "Listen port not specified, defaulting to %s",
self.listen_port) 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) # Get whether or not UPNP binds to multicast address (239.255.255.250)
# or to the unicast address (host_ip_addr) # or to the unicast address (host_ip_addr)
self.upnp_bind_multicast = conf.get( self.upnp_bind_multicast = conf.get(

View File

@ -68,50 +68,50 @@ xiaomi_miio_set_buzzer_on:
description: Turn the buzzer on. description: Turn the buzzer on.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_buzzer_off: xiaomi_miio_set_buzzer_off:
description: Turn the buzzer off. description: Turn the buzzer off.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_led_on: xiaomi_miio_set_led_on:
description: Turn the led on. description: Turn the led on.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_led_off: xiaomi_miio_set_led_off:
description: Turn the led off. description: Turn the led off.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_child_lock_on: xiaomi_miio_set_child_lock_on:
description: Turn the child lock on. description: Turn the child lock on.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_child_lock_off: xiaomi_miio_set_child_lock_off:
description: Turn the child lock off. description: Turn the child lock off.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
xiaomi_miio_set_favorite_level: xiaomi_miio_set_favorite_level:
description: Set the favorite level. description: Set the favorite level.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
level: level:
description: Level, between 0 and 16. description: Level, between 0 and 16.
example: 1 example: 1
@ -120,8 +120,87 @@ xiaomi_miio_set_led_brightness:
description: Set the led brightness. description: Set the led brightness.
fields: fields:
entity_id: entity_id:
description: Name of the air purifier entity. description: Name of the xiaomi miio entity.
example: 'fan.xiaomi_air_purifier' example: 'fan.xiaomi_miio_device'
brightness: brightness:
description: Brightness (0 = Bright, 1 = Dim, 2 = Off) description: Brightness (0 = Bright, 1 = Dim, 2 = Off)
example: 1 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 For more details about this platform, please refer to the documentation
https://home-assistant.io/components/fan.xiaomi_miio/ https://home-assistant.io/components/fan.xiaomi_miio/
""" """
import asyncio import asyncio
from enum import Enum
from functools import partial from functools import partial
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA, from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
SUPPORT_SET_SPEED, DOMAIN, ) SUPPORT_SET_SPEED, DOMAIN, )
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, 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__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Air Purifier' DEFAULT_NAME = 'Xiaomi Miio Device'
PLATFORM = 'xiaomi_miio' 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({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), 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_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'] REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
ATTR_MODEL = 'model'
# Air Purifier
ATTR_TEMPERATURE = 'temperature' ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity' ATTR_HUMIDITY = 'humidity'
ATTR_AIR_QUALITY_INDEX = 'aqi' ATTR_AIR_QUALITY_INDEX = 'aqi'
@ -45,20 +68,190 @@ ATTR_LED_BRIGHTNESS = 'led_brightness'
ATTR_MOTOR_SPEED = 'motor_speed' ATTR_MOTOR_SPEED = 'motor_speed'
ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi' ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi'
ATTR_PURIFY_VOLUME = 'purify_volume' ATTR_PURIFY_VOLUME = 'purify_volume'
ATTR_BRIGHTNESS = 'brightness' ATTR_BRIGHTNESS = 'brightness'
ATTR_LEVEL = 'level' 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'] 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_ON = 'xiaomi_miio_set_buzzer_on'
SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off' SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off'
SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on' SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on'
SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off' SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off'
SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on' SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on'
SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off' 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_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({ AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, 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)) 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_TO_METHOD = {
SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'}, SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'},
SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'}, 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_LED_OFF: {'method': 'async_set_led_off'},
SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'}, SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'},
SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'}, SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'},
SERVICE_SET_FAVORITE_LEVEL: { SERVICE_SET_AUTO_DETECT_ON: {'method': 'async_set_auto_detect_on'},
'method': 'async_set_favorite_level', SERVICE_SET_AUTO_DETECT_OFF: {'method': 'async_set_auto_detect_off'},
'schema': SERVICE_SCHEMA_FAVORITE_LEVEL}, 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: { SERVICE_SET_LED_BRIGHTNESS: {
'method': 'async_set_led_brightness', 'method': 'async_set_led_brightness',
'schema': SERVICE_SCHEMA_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 # pylint: disable=unused-argument
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up the air purifier from config.""" """Set up the miio fan device from config."""
from miio import AirPurifier, DeviceException from miio import Device, DeviceException
if PLATFORM not in hass.data: if DATA_KEY not in hass.data:
hass.data[PLATFORM] = {} hass.data[DATA_KEY] = {}
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
token = config.get(CONF_TOKEN) token = config.get(CONF_TOKEN)
model = config.get(CONF_MODEL)
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) _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) 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[DATA_KEY][host] = device
hass.data[PLATFORM][host] = xiaomi_air_purifier async_add_devices([device], update_before_add=True)
except DeviceException:
raise PlatformNotReady
async_add_devices([xiaomi_air_purifier], update_before_add=True) async def async_service_handler(service):
@asyncio.coroutine
def async_service_handler(service):
"""Map services to methods on XiaomiAirPurifier.""" """Map services to methods on XiaomiAirPurifier."""
method = SERVICE_TO_METHOD.get(service.service) method = SERVICE_TO_METHOD.get(service.service)
params = {key: value for key, value in service.data.items() params = {key: value for key, value in service.data.items()
if key != ATTR_ENTITY_ID} if key != ATTR_ENTITY_ID}
entity_ids = service.data.get(ATTR_ENTITY_ID) entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids: 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] device.entity_id in entity_ids]
else: else:
devices = hass.data[PLATFORM].values() devices = hass.data[DATA_KEY].values()
update_tasks = [] update_tasks = []
for device in devices: 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)) update_tasks.append(device.async_update_ha_state(True))
if update_tasks: 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: for air_purifier_service in SERVICE_TO_METHOD:
schema = SERVICE_TO_METHOD[air_purifier_service].get( 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) DOMAIN, air_purifier_service, async_service_handler, schema=schema)
class XiaomiAirPurifier(FanEntity): class XiaomiGenericDevice(FanEntity):
"""Representation of a Xiaomi Air Purifier.""" """Representation of a generic Xiaomi device."""
def __init__(self, name, air_purifier): def __init__(self, name, device, model, unique_id):
"""Initialize the air purifier.""" """Initialize the generic Xiaomi device."""
self._name = name 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 = None
self._state_attrs = { self._state_attrs = {
ATTR_AIR_QUALITY_INDEX: None, ATTR_MODEL: self._model,
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,
} }
self._device_features = FEATURE_FLAGS_GENERIC
self._skip_update = False self._skip_update = False
@property @property
@ -176,9 +415,14 @@ class XiaomiAirPurifier(FanEntity):
@property @property
def should_poll(self): def should_poll(self):
"""Poll the fan.""" """Poll the device."""
return True return True
@property
def unique_id(self):
"""Return an unique ID."""
return self._unique_id
@property @property
def name(self): def name(self):
"""Return the name of the device if any.""" """Return the name of the device if any."""
@ -187,7 +431,7 @@ class XiaomiAirPurifier(FanEntity):
@property @property
def available(self): def available(self):
"""Return true when state is known.""" """Return true when state is known."""
return self._state is not None return self._available
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -196,50 +440,116 @@ class XiaomiAirPurifier(FanEntity):
@property @property
def is_on(self): def is_on(self):
"""Return true if fan is on.""" """Return true if device is on."""
return self._state return self._state
@asyncio.coroutine @staticmethod
def _try_command(self, mask_error, func, *args, **kwargs): def _extract_value_from_attribute(state, attribute):
"""Call an air purifier command handling error messages.""" 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 from miio import DeviceException
try: try:
result = yield from self.hass.async_add_job( result = await self.hass.async_add_job(
partial(func, *args, **kwargs)) 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 return result == SUCCESS
except DeviceException as exc: except DeviceException as exc:
_LOGGER.error(mask_error, exc) _LOGGER.error(mask_error, exc)
self._available = False
return False return False
@asyncio.coroutine async def async_turn_on(self, speed: str = None,
def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None: **kwargs) -> None:
"""Turn the fan on.""" """Turn the device on."""
if speed: if speed:
# If operation mode was set the device must not be turned on. # 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: else:
result = yield from self._try_command( result = await self._try_command(
"Turning the air purifier on failed.", self._air_purifier.on) "Turning the miio device on failed.", self._device.on)
if result: if result:
self._state = True self._state = True
self._skip_update = True self._skip_update = True
@asyncio.coroutine async def async_turn_off(self, **kwargs) -> None:
def async_turn_off(self: ToggleEntity, **kwargs) -> None: """Turn the device off."""
"""Turn the fan off.""" result = await self._try_command(
result = yield from self._try_command( "Turning the miio device off failed.", self._device.off)
"Turning the air purifier off failed.", self._air_purifier.off)
if result: if result:
self._state = False self._state = False
self._skip_update = True self._skip_update = True
@asyncio.coroutine async def async_set_buzzer_on(self):
def async_update(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.""" """Fetch state from the device."""
from miio import DeviceException from miio import DeviceException
@ -249,40 +559,24 @@ class XiaomiAirPurifier(FanEntity):
return return
try: try:
state = yield from self.hass.async_add_job( state = await self.hass.async_add_job(
self._air_purifier.status) self._device.status)
_LOGGER.debug("Got new state: %s", state) _LOGGER.debug("Got new state: %s", state)
self._available = True
self._state = state.is_on self._state = state.is_on
self._state_attrs = { self._state_attrs.update(
ATTR_TEMPERATURE: state.temperature, {key: self._extract_value_from_attribute(state, value) for
ATTR_HUMIDITY: state.humidity, key, value in self._available_attributes.items()})
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
except DeviceException as ex: except DeviceException as ex:
self._state = None self._available = False
_LOGGER.error("Got exception while fetching the state: %s", ex) _LOGGER.error("Got exception while fetching the state: %s", ex)
@property @property
def speed_list(self: ToggleEntity) -> list: def speed_list(self) -> list:
"""Get the list of available speeds.""" """Get the list of available speeds."""
from miio.airpurifier import OperationMode return self._speed_list
return [mode.name for mode in OperationMode]
@property @property
def speed(self): def speed(self):
@ -294,70 +588,227 @@ class XiaomiAirPurifier(FanEntity):
return None return None
@asyncio.coroutine async def async_set_speed(self, speed: str) -> None:
def async_set_speed(self: ToggleEntity, speed: str) -> None:
"""Set the speed of the fan.""" """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 from miio.airpurifier import OperationMode
yield from self._try_command( _LOGGER.debug("Setting the operation mode to: %s", speed)
"Setting operation mode of the air purifier failed.",
self._air_purifier.set_mode, OperationMode[speed.title()])
@asyncio.coroutine await self._try_command(
def async_set_buzzer_on(self): "Setting operation mode of the miio device failed.",
"""Turn the buzzer on.""" self._device.set_mode, OperationMode[speed.title()])
yield from self._try_command(
"Turning the buzzer of the air purifier on failed.",
self._air_purifier.set_buzzer, True)
@asyncio.coroutine async def async_set_led_on(self):
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):
"""Turn the led on.""" """Turn the led on."""
yield from self._try_command( if self._device_features & FEATURE_SET_LED == 0:
"Turning the led of the air purifier off failed.", return
self._air_purifier.set_led, True)
@asyncio.coroutine await self._try_command(
def async_set_led_off(self): "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.""" """Turn the led off."""
yield from self._try_command( if self._device_features & FEATURE_SET_LED == 0:
"Turning the led of the air purifier off failed.", return
self._air_purifier.set_led, False)
@asyncio.coroutine await self._try_command(
def async_set_child_lock_on(self): "Turning the led of the miio device off failed.",
"""Turn the child lock on.""" self._device.set_led, False)
yield from self._try_command(
"Turning the child lock of the air purifier on failed.",
self._air_purifier.set_child_lock, True)
@asyncio.coroutine async def async_set_led_brightness(self, brightness: int = 2):
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):
"""Set the led brightness.""" """Set the led brightness."""
if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0:
return
from miio.airpurifier import LedBrightness from miio.airpurifier import LedBrightness
yield from self._try_command( await self._try_command(
"Setting the led brightness of the air purifier failed.", "Setting the led brightness of the miio device failed.",
self._air_purifier.set_led_brightness, LedBrightness(brightness)) self._device.set_led_brightness, LedBrightness(brightness))
@asyncio.coroutine async def async_set_favorite_level(self, level: int = 1):
def async_set_favorite_level(self, level: int = 1):
"""Set the favorite level.""" """Set the favorite level."""
yield from self._try_command( if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0:
"Setting the favorite level of the air purifier failed.", return
self._air_purifier.set_favorite_level, level)
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.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20180401.0'] REQUIREMENTS = ['home-assistant-frontend==20180404.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']

View File

@ -28,7 +28,7 @@ from .util import (
TYPES = Registry() TYPES = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['HAP-python==1.1.7'] REQUIREMENTS = ['HAP-python==1.1.8']
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
@ -102,8 +102,7 @@ def get_accessory(hass, state, aid, config):
aid=aid) aid=aid)
elif state.domain == 'alarm_control_panel': elif state.domain == 'alarm_control_panel':
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem')
'SecuritySystem')
return TYPES['SecuritySystem'](hass, state.entity_id, state.name, return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
alarm_code=config.get(ATTR_CODE), alarm_code=config.get(ATTR_CODE),
aid=aid) aid=aid)
@ -120,6 +119,7 @@ def get_accessory(hass, state, aid, config):
state.name, support_auto, aid=aid) state.name, support_auto, aid=aid)
elif state.domain == 'light': 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) return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
elif state.domain == 'switch' or state.domain == 'remote' \ 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 ( from .const import (
ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL,
CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) CHAR_NAME, CHAR_SERIAL_NUMBER)
from .util import ( from .util import (
show_setup_message, dismiss_setup_message) 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) 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): class HomeAccessory(Accessory):
"""Adapter class for Accessory.""" """Adapter class for Accessory."""
@ -65,10 +56,10 @@ class HomeAccessory(Accessory):
def run(self): def run(self):
"""Method called by accessory after driver is started.""" """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) self.update_state(new_state=state)
async_track_state_change( async_track_state_change(
self._hass, self._entity_id, self.update_state) self.hass, self.entity_id, self.update_state)
class HomeBridge(Bridge): class HomeBridge(Bridge):
@ -79,11 +70,10 @@ class HomeBridge(Bridge):
"""Initialize a Bridge object.""" """Initialize a Bridge object."""
super().__init__(name, **kwargs) super().__init__(name, **kwargs)
set_accessory_info(self, name, model) set_accessory_info(self, name, model)
self._hass = hass self.hass = hass
def _set_services(self): def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO) add_preload_service(self, SERV_ACCESSORY_INFO)
add_preload_service(self, SERV_BRIDGING_STATE)
def setup_message(self): def setup_message(self):
"""Prevent print of pyhap setup message to terminal.""" """Prevent print of pyhap setup message to terminal."""
@ -92,12 +82,12 @@ class HomeBridge(Bridge):
def add_paired_client(self, client_uuid, client_public): def add_paired_client(self, client_uuid, client_public):
"""Override super function to dismiss setup message if paired.""" """Override super function to dismiss setup message if paired."""
super().add_paired_client(client_uuid, client_public) 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): def remove_paired_client(self, client_uuid):
"""Override super function to show setup message if unpaired.""" """Override super function to show setup message if unpaired."""
super().remove_paired_client(client_uuid) super().remove_paired_client(client_uuid)
show_setup_message(self, self._hass) show_setup_message(self, self.hass)
class HomeDriver(AccessoryDriver): class HomeDriver(AccessoryDriver):

View File

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

View File

@ -6,8 +6,8 @@ from homeassistant.components.cover import ATTR_CURRENT_POSITION
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import HomeAccessory, add_preload_service
from .const import ( from .const import (
SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
CHAR_TARGET_POSITION, CHAR_POSITION_STATE) CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -20,13 +20,13 @@ class WindowCovering(HomeAccessory):
The cover entity must support: set_cover_position. 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.""" """Initialize a WindowCovering accessory object."""
super().__init__(display_name, entity_id, 'WINDOW_COVERING', super().__init__(display_name, entity_id,
*args, **kwargs) CATEGORY_WINDOW_COVERING, **kwargs)
self._hass = hass self.hass = hass
self._entity_id = entity_id self.entity_id = entity_id
self.current_position = None self.current_position = None
self.homekit_target = None self.homekit_target = None
@ -48,14 +48,14 @@ class WindowCovering(HomeAccessory):
"""Move cover to value if call came from HomeKit.""" """Move cover to value if call came from HomeKit."""
self.char_target_position.set_value(value, should_callback=False) self.char_target_position.set_value(value, should_callback=False)
if value != self.current_position: 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 self.homekit_target = value
if value > self.current_position: if value > self.current_position:
self.char_position_state.set_value(1) self.char_position_state.set_value(1)
elif value < self.current_position: elif value < self.current_position:
self.char_position_state.set_value(0) self.char_position_state.set_value(0)
self._hass.components.cover.set_cover_position( self.hass.components.cover.set_cover_position(
value, self._entity_id) value, self.entity_id)
def update_state(self, entity_id=None, old_state=None, new_state=None): def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update cover position after state changed.""" """Update cover position after state changed."""
@ -63,14 +63,11 @@ class WindowCovering(HomeAccessory):
return return
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
if current_position is None: if isinstance(current_position, int):
return self.current_position = current_position
self.char_current_position.set_value(self.current_position)
self.current_position = int(current_position) if self.homekit_target is None or \
self.char_current_position.set_value(self.current_position) abs(self.current_position - self.homekit_target) < 6:
self.char_target_position.set_value(self.current_position)
if self.homekit_target is None or \ self.char_position_state.set_value(2)
abs(self.current_position - self.homekit_target) < 6: self.homekit_target = None
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 import logging
from homeassistant.components.light import ( 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 homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import HomeAccessory, add_preload_service
from .const import ( from .const import (
CATEGORY_LIGHT, SERV_LIGHTBULB, CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -20,25 +21,27 @@ RGB_COLOR = 'rgb_color'
class Light(HomeAccessory): class Light(HomeAccessory):
"""Generate a Light accessory for a light entity. """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.""" """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.hass = hass
self._entity_id = entity_id self.entity_id = entity_id
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
CHAR_HUE: False, CHAR_SATURATION: False, CHAR_HUE: False, CHAR_SATURATION: False,
RGB_COLOR: False} CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
self._state = 0 self._state = 0
self.chars = [] 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) .attributes.get(ATTR_SUPPORTED_FEATURES)
if self._features & SUPPORT_BRIGHTNESS: if self._features & SUPPORT_BRIGHTNESS:
self.chars.append(CHAR_BRIGHTNESS) self.chars.append(CHAR_BRIGHTNESS)
if self._features & SUPPORT_COLOR_TEMP:
self.chars.append(CHAR_COLOR_TEMPERATURE)
if self._features & SUPPORT_COLOR: if self._features & SUPPORT_COLOR:
self.chars.append(CHAR_HUE) self.chars.append(CHAR_HUE)
self.chars.append(CHAR_SATURATION) self.chars.append(CHAR_SATURATION)
@ -55,6 +58,18 @@ class Light(HomeAccessory):
.get_characteristic(CHAR_BRIGHTNESS) .get_characteristic(CHAR_BRIGHTNESS)
self.char_brightness.setter_callback = self.set_brightness self.char_brightness.setter_callback = self.set_brightness
self.char_brightness.value = 0 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: if CHAR_HUE in self.chars:
self.char_hue = serv_light.get_characteristic(CHAR_HUE) self.char_hue = serv_light.get_characteristic(CHAR_HUE)
self.char_hue.setter_callback = self.set_hue self.char_hue.setter_callback = self.set_hue
@ -70,29 +85,36 @@ class Light(HomeAccessory):
if self._state == value: if self._state == value:
return 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._flag[CHAR_ON] = True
self.char_on.set_value(value, should_callback=False) self.char_on.set_value(value, should_callback=False)
if value == 1: if value == 1:
self._hass.components.light.turn_on(self._entity_id) self.hass.components.light.turn_on(self.entity_id)
elif value == 0: 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): def set_brightness(self, value):
"""Set brightness if call came from HomeKit.""" """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._flag[CHAR_BRIGHTNESS] = True
self.char_brightness.set_value(value, should_callback=False) self.char_brightness.set_value(value, should_callback=False)
if value != 0: if value != 0:
self._hass.components.light.turn_on( self.hass.components.light.turn_on(
self._entity_id, brightness_pct=value) self.entity_id, brightness_pct=value)
else: 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): def set_saturation(self, value):
"""Set saturation if call came from HomeKit.""" """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._flag[CHAR_SATURATION] = True
self.char_saturation.set_value(value, should_callback=False) self.char_saturation.set_value(value, should_callback=False)
self._saturation = value self._saturation = value
@ -100,7 +122,7 @@ class Light(HomeAccessory):
def set_hue(self, value): def set_hue(self, value):
"""Set hue if call came from HomeKit.""" """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._flag[CHAR_HUE] = True
self.char_hue.set_value(value, should_callback=False) self.char_hue.set_value(value, should_callback=False)
self._hue = value self._hue = value
@ -112,11 +134,11 @@ class Light(HomeAccessory):
if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \ if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \
self._flag[CHAR_SATURATION]: self._flag[CHAR_SATURATION]:
color = (self._hue, self._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({ self._flag.update({
CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True})
self._hass.components.light.turn_on( self.hass.components.light.turn_on(
self._entity_id, hs_color=color) self.entity_id, hs_color=color)
def update_state(self, entity_id=None, old_state=None, new_state=None): def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update light after state change.""" """Update light after state change."""
@ -141,13 +163,25 @@ class Light(HomeAccessory):
should_callback=False) should_callback=False)
self._flag[CHAR_BRIGHTNESS] = 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 # Handle Color
if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars:
hue, saturation = new_state.attributes.get( hue, saturation = new_state.attributes.get(
ATTR_HS_COLOR, (None, None)) ATTR_HS_COLOR, (None, None))
if not self._flag[RGB_COLOR] and ( 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_hue.set_value(hue, should_callback=False)
self.char_saturation.set_value(saturation, self.char_saturation.set_value(saturation,
should_callback=False) should_callback=False)
self._hue, self._saturation = (hue, saturation)
self._flag[RGB_COLOR] = False self._flag[RGB_COLOR] = False

View File

@ -9,8 +9,8 @@ from homeassistant.const import (
from . import TYPES from . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import HomeAccessory, add_preload_service
from .const import ( from .const import (
SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM,
CHAR_TARGET_SECURITY_STATE) CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -27,14 +27,13 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
class SecuritySystem(HomeAccessory): class SecuritySystem(HomeAccessory):
"""Generate an SecuritySystem accessory for an alarm control panel.""" """Generate an SecuritySystem accessory for an alarm control panel."""
def __init__(self, hass, entity_id, display_name, def __init__(self, hass, entity_id, display_name, alarm_code, **kwargs):
alarm_code, *args, **kwargs):
"""Initialize a SecuritySystem accessory object.""" """Initialize a SecuritySystem accessory object."""
super().__init__(display_name, entity_id, 'ALARM_SYSTEM', super().__init__(display_name, entity_id,
*args, **kwargs) CATEGORY_ALARM_SYSTEM, **kwargs)
self._hass = hass self.hass = hass
self._entity_id = entity_id self.entity_id = entity_id
self._alarm_code = alarm_code self._alarm_code = alarm_code
self.flag_target_state = False self.flag_target_state = False
@ -52,16 +51,16 @@ class SecuritySystem(HomeAccessory):
def set_security_state(self, value): def set_security_state(self, value):
"""Move security state to value if call came from HomeKit.""" """Move security state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set security state to %d', _LOGGER.debug('%s: Set security state to %d',
self._entity_id, value) self.entity_id, value)
self.flag_target_state = True self.flag_target_state = True
self.char_target_state.set_value(value, should_callback=False) self.char_target_state.set_value(value, should_callback=False)
hass_value = HOMEKIT_TO_HASS[value] hass_value = HOMEKIT_TO_HASS[value]
service = STATE_TO_SERVICE[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: if self._alarm_code:
params[ATTR_CODE] = 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): def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update security state after state changed.""" """Update security state after state changed."""
@ -76,7 +75,7 @@ class SecuritySystem(HomeAccessory):
self.char_current_state.set_value(current_security_state, self.char_current_state.set_value(current_security_state,
should_callback=False) should_callback=False)
_LOGGER.debug('%s: Updated current state to %s (%d)', _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: if not self.flag_target_state:
self.char_target_state.set_value(current_security_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) ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
from . import TYPES from . import TYPES
from .accessories import ( from .accessories import HomeAccessory, add_preload_service
HomeAccessory, add_preload_service, override_properties)
from .const import ( from .const import (
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
@ -23,16 +22,16 @@ class TemperatureSensor(HomeAccessory):
Sensor entity must return temperature in °C, °F. 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.""" """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.hass = hass
self._entity_id = entity_id self.entity_id = entity_id
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE) 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.char_temp.value = 0
self.unit = None self.unit = None
@ -47,7 +46,7 @@ class TemperatureSensor(HomeAccessory):
temperature = temperature_to_homekit(temperature, unit) temperature = temperature_to_homekit(temperature, unit)
self.char_temp.set_value(temperature, should_callback=False) self.char_temp.set_value(temperature, should_callback=False)
_LOGGER.debug('%s: Current temperature set to %d°C', _LOGGER.debug('%s: Current temperature set to %d°C',
self._entity_id, temperature) self.entity_id, temperature)
@TYPES.register('HumiditySensor') @TYPES.register('HumiditySensor')
@ -58,8 +57,8 @@ class HumiditySensor(HomeAccessory):
"""Initialize a HumiditySensor accessory object.""" """Initialize a HumiditySensor accessory object."""
super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
self._hass = hass self.hass = hass
self._entity_id = entity_id self.entity_id = entity_id
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
self.char_humidity = serv_humidity \ self.char_humidity = serv_humidity \
@ -75,4 +74,4 @@ class HumiditySensor(HomeAccessory):
if humidity: if humidity:
self.char_humidity.set_value(humidity, should_callback=False) self.char_humidity.set_value(humidity, should_callback=False)
_LOGGER.debug('%s: Percent set to %d%%', _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 . import TYPES
from .accessories import HomeAccessory, add_preload_service 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__) _LOGGER = logging.getLogger(__name__)
@ -16,12 +16,12 @@ _LOGGER = logging.getLogger(__name__)
class Switch(HomeAccessory): class Switch(HomeAccessory):
"""Generate a Switch accessory.""" """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.""" """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.hass = hass
self._entity_id = entity_id self.entity_id = entity_id
self._domain = split_entity_id(entity_id)[0] self._domain = split_entity_id(entity_id)[0]
self.flag_target_state = False self.flag_target_state = False
@ -34,12 +34,12 @@ class Switch(HomeAccessory):
def set_state(self, value): def set_state(self, value):
"""Move switch state to value if call came from HomeKit.""" """Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state to %s', _LOGGER.debug('%s: Set switch state to %s',
self._entity_id, value) self.entity_id, value)
self.flag_target_state = True self.flag_target_state = True
self.char_on.set_value(value, should_callback=False) self.char_on.set_value(value, should_callback=False)
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self._hass.services.call(self._domain, service, self.hass.services.call(self._domain, service,
{ATTR_ENTITY_ID: self._entity_id}) {ATTR_ENTITY_ID: self.entity_id})
def update_state(self, entity_id=None, old_state=None, new_state=None): def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update switch state after state changed.""" """Update switch state after state changed."""
@ -49,7 +49,7 @@ class Switch(HomeAccessory):
current_state = (new_state.state == STATE_ON) current_state = (new_state.state == STATE_ON)
if not self.flag_target_state: if not self.flag_target_state:
_LOGGER.debug('%s: Set current state to %s', _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.char_on.set_value(current_state, should_callback=False)
self.flag_target_state = False self.flag_target_state = False

View File

@ -7,12 +7,12 @@ from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
STATE_HEAT, STATE_COOL, STATE_AUTO) STATE_HEAT, STATE_COOL, STATE_AUTO)
from homeassistant.const import ( 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 . import TYPES
from .accessories import HomeAccessory, add_preload_service from .accessories import HomeAccessory, add_preload_service
from .const import ( 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_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS,
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) 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__) _LOGGER = logging.getLogger(__name__)
STATE_OFF = 'off'
UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} 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()} 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, 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): class Thermostat(HomeAccessory):
"""Generate a Thermostat accessory for a climate.""" """Generate a Thermostat accessory for a climate."""
def __init__(self, hass, entity_id, display_name, def __init__(self, hass, entity_id, display_name, support_auto, **kwargs):
support_auto, *args, **kwargs):
"""Initialize a Thermostat accessory object.""" """Initialize a Thermostat accessory object."""
super().__init__(display_name, entity_id, 'THERMOSTAT', super().__init__(display_name, entity_id,
*args, **kwargs) CATEGORY_THERMOSTAT, **kwargs)
self._hass = hass self.hass = hass
self._entity_id = entity_id self.entity_id = entity_id
self._call_timer = None self._call_timer = None
self._unit = TEMP_CELSIUS self._unit = TEMP_CELSIUS
@ -101,48 +99,48 @@ class Thermostat(HomeAccessory):
"""Move operation mode to value if call came from HomeKit.""" """Move operation mode to value if call came from HomeKit."""
self.char_target_heat_cool.set_value(value, should_callback=False) self.char_target_heat_cool.set_value(value, should_callback=False)
if value in HC_HOMEKIT_TO_HASS: 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 self.heat_cool_flag_target_state = True
hass_value = HC_HOMEKIT_TO_HASS[value] hass_value = HC_HOMEKIT_TO_HASS[value]
self._hass.components.climate.set_operation_mode( self.hass.components.climate.set_operation_mode(
operation_mode=hass_value, entity_id=self._entity_id) operation_mode=hass_value, entity_id=self.entity_id)
def set_cooling_threshold(self, value): def set_cooling_threshold(self, value):
"""Set cooling threshold temp to value if call came from HomeKit.""" """Set cooling threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', _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.coolingthresh_flag_target_state = True
self.char_cooling_thresh_temp.set_value(value, should_callback=False) self.char_cooling_thresh_temp.set_value(value, should_callback=False)
low = self.char_heating_thresh_temp.value low = self.char_heating_thresh_temp.value
low = temperature_to_states(low, self._unit) low = temperature_to_states(low, self._unit)
value = temperature_to_states(value, self._unit) value = temperature_to_states(value, self._unit)
self._hass.components.climate.set_temperature( self.hass.components.climate.set_temperature(
entity_id=self._entity_id, target_temp_high=value, entity_id=self.entity_id, target_temp_high=value,
target_temp_low=low) target_temp_low=low)
def set_heating_threshold(self, value): def set_heating_threshold(self, value):
"""Set heating threshold temp to value if call came from HomeKit.""" """Set heating threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', _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.heatingthresh_flag_target_state = True
self.char_heating_thresh_temp.set_value(value, should_callback=False) self.char_heating_thresh_temp.set_value(value, should_callback=False)
# Home assistant always wants to set low and high at the same time # Home assistant always wants to set low and high at the same time
high = self.char_cooling_thresh_temp.value high = self.char_cooling_thresh_temp.value
high = temperature_to_states(high, self._unit) high = temperature_to_states(high, self._unit)
value = temperature_to_states(value, self._unit) value = temperature_to_states(value, self._unit)
self._hass.components.climate.set_temperature( self.hass.components.climate.set_temperature(
entity_id=self._entity_id, target_temp_high=high, entity_id=self.entity_id, target_temp_high=high,
target_temp_low=value) target_temp_low=value)
def set_target_temperature(self, value): def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit.""" """Set target temperature to value if call came from HomeKit."""
_LOGGER.debug('%s: Set target temperature to %.2f°C', _LOGGER.debug('%s: Set target temperature to %.2f°C',
self._entity_id, value) self.entity_id, value)
self.temperature_flag_target_state = True self.temperature_flag_target_state = True
self.char_target_temp.set_value(value, should_callback=False) self.char_target_temp.set_value(value, should_callback=False)
value = temperature_to_states(value, self._unit) value = temperature_to_states(value, self._unit)
self._hass.components.climate.set_temperature( self.hass.components.climate.set_temperature(
temperature=value, entity_id=self._entity_id) temperature=value, entity_id=self.entity_id)
def update_state(self, entity_id=None, old_state=None, new_state=None): def update_state(self, entity_id=None, old_state=None, new_state=None):
"""Update security state after state changed.""" """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 For more details about this component, please refer to the documentation at
https://home-assistant.io/components/hue/ https://home-assistant.io/components/hue/
""" """
import asyncio
import json
import ipaddress import ipaddress
import logging import logging
import os
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.discovery import SERVICE_HUE
from homeassistant.const import CONF_FILENAME, CONF_HOST from homeassistant.const import CONF_FILENAME, CONF_HOST
import homeassistant.helpers.config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers import discovery, aiohttp_client
from homeassistant import config_entries from .const import DOMAIN, API_NUPNP
from homeassistant.util.json import save_json from .bridge import HueBridge
# Loading the config flow file will register the flow
from .config_flow import configured_hosts
REQUIREMENTS = ['aiohue==1.3.0'] REQUIREMENTS = ['aiohue==1.3.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "hue"
SERVICE_HUE_SCENE = "hue_activate_scene"
API_NUPNP = 'https://www.meethue.com/api/nupnp'
CONF_BRIDGES = "bridges" CONF_BRIDGES = "bridges"
CONF_ALLOW_UNREACHABLE = 'allow_unreachable' CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
@ -42,6 +34,7 @@ DEFAULT_ALLOW_HUE_GROUPS = True
BRIDGE_CONFIG_SCHEMA = vol.Schema({ BRIDGE_CONFIG_SCHEMA = vol.Schema({
# Validate as IP address and then convert back to a string. # Validate as IP address and then convert back to a string.
vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.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_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
vol.Optional(CONF_ALLOW_UNREACHABLE, vol.Optional(CONF_ALLOW_UNREACHABLE,
default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean, default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
@ -56,19 +49,6 @@ CONFIG_SCHEMA = vol.Schema({
}), }),
}, extra=vol.ALLOW_EXTRA) }, 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): async def async_setup(hass, config):
"""Set up the Hue platform.""" """Set up the Hue platform."""
@ -76,20 +56,8 @@ async def async_setup(hass, config):
if conf is None: if conf is None:
conf = {} conf = {}
if DOMAIN not in hass.data: hass.data[DOMAIN] = {}
hass.data[DOMAIN] = {} configured = configured_hosts(hass)
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)
# User has configured bridges # User has configured bridges
if CONF_BRIDGES in conf: if CONF_BRIDGES in conf:
@ -103,12 +71,19 @@ async def async_setup(hass, config):
async with websession.get(API_NUPNP) as req: async with websession.get(API_NUPNP) as req:
hosts = await req.json() hosts = await req.json()
# Run through config schema to populate defaults bridges = []
bridges = [BRIDGE_CONFIG_SCHEMA({ for entry in hosts:
CONF_HOST: entry['internalipaddress'], # Filter out already configured hosts
CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), if entry['internalipaddress'] in configured:
}) for entry in hosts] 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: else:
# Component not specified in config, we're loaded via discovery # Component not specified in config, we're loaded via discovery
bridges = [] bridges = []
@ -116,277 +91,43 @@ async def async_setup(hass, config):
if not bridges: if not bridges:
return True return True
await asyncio.wait([ for bridge_conf in bridges:
async_setup_bridge( host = bridge_conf[CONF_HOST]
hass, bridge[CONF_HOST], bridge[CONF_FILENAME],
bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS] # Store config in hass.data so the config entry can find it
) for bridge in bridges 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 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): async def async_setup_entry(hass, entry):
"""Set up a bridge for a config entry.""" """Set up a bridge from a config entry."""
await async_setup_bridge(hass, entry.data['host'], host = entry.data['host']
username=entry.data['username']) config = hass.data[DOMAIN].get(host)
return True
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": { "abort": {
"discover_timeout": "Unable to discover Hue bridges", "discover_timeout": "Unable to discover Hue bridges",
"no_bridges": "No Philips Hue bridges discovered", "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 import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['insteonplm==0.8.3'] REQUIREMENTS = ['insteonplm==0.8.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

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

View File

@ -300,8 +300,14 @@ class HueLight(Light):
command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
if ATTR_HS_COLOR in kwargs: if ATTR_HS_COLOR in kwargs:
command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) if self.is_osram:
command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) 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: elif ATTR_COLOR_TEMP in kwargs:
temp = kwargs[ATTR_COLOR_TEMP] temp = kwargs[ATTR_COLOR_TEMP]
command['ct'] = max(self.min_mireds, min(temp, self.max_mireds)) command['ct'] = max(self.min_mireds, min(temp, self.max_mireds))

View File

@ -79,7 +79,7 @@ class IGloLamp(Light):
@property @property
def hs_color(self): def hs_color(self):
"""Return the hs value.""" """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 @property
def effect(self): def effect(self):

View File

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

View File

@ -15,7 +15,7 @@ from homeassistant.components.light import (
ATTR_HS_COLOR) ATTR_HS_COLOR)
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN 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__) _LOGGER = logging.getLogger(__name__)
@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the myStrom Light platform.""" """Set up the myStrom Light platform."""
from pymystrom import MyStromBulb from pymystrom.bulb import MyStromBulb
from pymystrom.exceptions import MyStromConnectionError from pymystrom.exceptions import MyStromConnectionError
host = config.get(CONF_HOST) 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.qwikswitch/ 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 DEPENDENCIES = [QWIKSWITCH]
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['qwikswitch']
# pylint: disable=unused-argument async def async_setup_platform(hass, _, add_devices, discovery_info=None):
def setup_platform(hass, config, add_devices, discovery_info=None): """Add lights from the main Qwikswitch component."""
"""Set up the lights from the main Qwikswitch component."""
if discovery_info is None: if discovery_info is None:
_LOGGER.error("Configure Qwikswitch component failed") return
return False
add_devices(qwikswitch.QSUSB['light']) qsusb = hass.data[QWIKSWITCH]
return True 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: color_name:
description: A human readable color name. description: A human readable color name.
example: 'red' 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: xy_color:
description: Color for the light in XY-format. description: Color for the light in XY-format.
example: '[0.52, 0.43]' example: '[0.52, 0.43]'
@ -179,3 +182,13 @@ xiaomi_miio_set_delayed_turn_off:
time_period: time_period:
description: Time period for the delayed turn off. description: Time period for the delayed turn off.
example: "5, '0:05', {'minutes': 5}" 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.tradfri/ https://home-assistant.io/components/light.tradfri/
""" """
import asyncio
import logging import logging
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION,
SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP,
@ -17,20 +15,20 @@ from homeassistant.components.light import \
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA
from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \ from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \
KEY_API KEY_API
from homeassistant.util import color as color_util import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_TRANSITION_TIME = 'transition_time'
DEPENDENCIES = ['tradfri'] DEPENDENCIES = ['tradfri']
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
IKEA = 'IKEA of Sweden' IKEA = 'IKEA of Sweden'
TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager' TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager'
SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION)
ALLOWED_TEMPERATURES = {IKEA}
@asyncio.coroutine async def async_setup_platform(hass, config,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices, discovery_info=None):
"""Set up the IKEA Tradfri Light platform.""" """Set up the IKEA Tradfri Light platform."""
if discovery_info is None: if discovery_info is None:
return return
@ -40,41 +38,43 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
gateway = hass.data[KEY_GATEWAY][gateway_id] gateway = hass.data[KEY_GATEWAY][gateway_id]
devices_command = gateway.get_devices() devices_command = gateway.get_devices()
devices_commands = yield from api(devices_command) devices_commands = await api(devices_command)
devices = yield from api(devices_commands) devices = await api(devices_commands)
lights = [dev for dev in devices if dev.has_light_control] lights = [dev for dev in devices if dev.has_light_control]
if lights: 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] allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id]
if allow_tradfri_groups: if allow_tradfri_groups:
groups_command = gateway.get_groups() groups_command = gateway.get_groups()
groups_commands = yield from api(groups_command) groups_commands = await api(groups_command)
groups = yield from api(groups_commands) groups = await api(groups_commands)
if groups: 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): class TradfriGroup(Light):
"""The platform class required by hass.""" """The platform class required by hass."""
def __init__(self, light, api): def __init__(self, group, api, gateway_id):
"""Initialize a Group.""" """Initialize a Group."""
self._api = api self._api = api
self._group = light self._unique_id = "group-{}-{}".format(gateway_id, group.id)
self._name = light.name self._group = group
self._name = group.name
self._refresh(light) self._refresh(group)
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Start thread when added to hass.""" """Start thread when added to hass."""
self._async_start_observe() self._async_start_observe()
@property @property
def should_poll(self): def unique_id(self):
"""No polling needed for tradfri group.""" """Return unique ID for this group."""
return False return self._unique_id
@property @property
def supported_features(self): def supported_features(self):
@ -96,13 +96,11 @@ class TradfriGroup(Light):
"""Return the brightness of the group lights.""" """Return the brightness of the group lights."""
return self._group.dimmer return self._group.dimmer
@asyncio.coroutine async def async_turn_off(self, **kwargs):
def async_turn_off(self, **kwargs):
"""Instruct the group lights to turn off.""" """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 async def async_turn_on(self, **kwargs):
def async_turn_on(self, **kwargs):
"""Instruct the group lights to turn on, or dim.""" """Instruct the group lights to turn on, or dim."""
keys = {} keys = {}
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
@ -112,16 +110,16 @@ class TradfriGroup(Light):
if kwargs[ATTR_BRIGHTNESS] == 255: if kwargs[ATTR_BRIGHTNESS] == 255:
kwargs[ATTR_BRIGHTNESS] = 254 kwargs[ATTR_BRIGHTNESS] = 254
yield from self._api( await self._api(
self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys))
else: else:
yield from self._api(self._group.set_state(1)) await self._api(self._group.set_state(1))
@callback @callback
def _async_start_observe(self, exc=None): def _async_start_observe(self, exc=None):
"""Start observation of light.""" """Start observation of light."""
# pylint: disable=import-error # pylint: disable=import-error
from pytradfri.error import PyTradFriError from pytradfri.error import PytradfriError
if exc: if exc:
_LOGGER.warning("Observation failed for %s", self._name, _LOGGER.warning("Observation failed for %s", self._name,
exc_info=exc) exc_info=exc)
@ -131,7 +129,7 @@ class TradfriGroup(Light):
err_callback=self._async_start_observe, err_callback=self._async_start_observe,
duration=0) duration=0)
self.hass.async_add_job(self._api(cmd)) 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) _LOGGER.warning("Observation failed, trying again", exc_info=err)
self._async_start_observe() self._async_start_observe()
@ -146,54 +144,44 @@ class TradfriGroup(Light):
self._refresh(tradfri_device) self._refresh(tradfri_device)
self.async_schedule_update_ha_state() 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): class TradfriLight(Light):
"""The platform class required by Home Assistant.""" """The platform class required by Home Assistant."""
def __init__(self, light, api): def __init__(self, light, api, gateway_id):
"""Initialize a Light.""" """Initialize a Light."""
self._api = api self._api = api
self._unique_id = "light-{}-{}".format(gateway_id, light.id)
self._light = None self._light = None
self._light_control = None self._light_control = None
self._light_data = None self._light_data = None
self._name = None self._name = None
self._hs_color = None self._hs_color = None
self._features = SUPPORTED_FEATURES self._features = SUPPORTED_FEATURES
self._temp_supported = False
self._available = True self._available = True
self._refresh(light) self._refresh(light)
@property
def unique_id(self):
"""Return unique ID for light."""
return self._unique_id
@property @property
def min_mireds(self): def min_mireds(self):
"""Return the coldest color_temp that this light supports.""" """Return the coldest color_temp that this light supports."""
if self._light_control.max_kelvin is not None: return self._light_control.min_mireds
return color_util.color_temperature_kelvin_to_mired(
self._light_control.max_kelvin
)
@property @property
def max_mireds(self): def max_mireds(self):
"""Return the warmest color_temp that this light supports.""" """Return the warmest color_temp that this light supports."""
if self._light_control.min_kelvin is not None: return self._light_control.max_mireds
return color_util.color_temperature_kelvin_to_mired(
self._light_control.min_kelvin
)
@property async def async_added_to_hass(self):
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):
"""Start thread when added to hass.""" """Start thread when added to hass."""
self._async_start_observe() self._async_start_observe()
@ -229,64 +217,87 @@ class TradfriLight(Light):
@property @property
def color_temp(self): def color_temp(self):
"""Return the CT color value in mireds.""" """Return the color temp value in mireds."""
kelvin_color = self._light_data.kelvin_color_inferred return self._light_data.color_temp
if kelvin_color is not None:
return color_util.color_temperature_kelvin_to_mired(
kelvin_color
)
@property @property
def hs_color(self): def hs_color(self):
"""HS color of the light.""" """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 async def async_turn_off(self, **kwargs):
def async_turn_off(self, **kwargs):
"""Instruct the light to turn off.""" """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 async def async_turn_on(self, **kwargs):
def async_turn_on(self, **kwargs): """Instruct the light to turn on."""
""" params = {}
Instruct the light to turn on. transition_time = None
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 = {}
if ATTR_TRANSITION in kwargs: 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: brightness = kwargs.get(ATTR_BRIGHTNESS)
if kwargs[ATTR_BRIGHTNESS] == 255:
kwargs[ATTR_BRIGHTNESS] = 254
yield from self._api( if brightness is not None:
self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS], if brightness > 254:
**keys)) 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: else:
yield from self._api( await self._api(
self._light_control.set_state(True)) self._light_control.set_state(True))
@callback @callback
def _async_start_observe(self, exc=None): def _async_start_observe(self, exc=None):
"""Start observation of light.""" """Start observation of light."""
# pylint: disable=import-error # pylint: disable=import-error
from pytradfri.error import PyTradFriError from pytradfri.error import PytradfriError
if exc: if exc:
_LOGGER.warning("Observation failed for %s", self._name, _LOGGER.warning("Observation failed for %s", self._name,
exc_info=exc) exc_info=exc)
@ -296,7 +307,7 @@ class TradfriLight(Light):
err_callback=self._async_start_observe, err_callback=self._async_start_observe,
duration=0) duration=0)
self.hass.async_add_job(self._api(cmd)) 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) _LOGGER.warning("Observation failed, trying again", exc_info=err)
self._async_start_observe() self._async_start_observe()
@ -309,27 +320,15 @@ class TradfriLight(Light):
self._light_control = light.light_control self._light_control = light.light_control
self._light_data = light.light_control.lights[0] self._light_data = light.light_control.lights[0]
self._name = light.name self._name = light.name
self._hs_color = None
self._features = SUPPORTED_FEATURES self._features = SUPPORTED_FEATURES
if self._light.device_info.manufacturer == IKEA: if light.light_control.can_set_color:
if self._light_control.can_set_kelvin: self._features |= SUPPORT_COLOR
self._features |= SUPPORT_COLOR_TEMP if light.light_control.can_set_temp:
if self._light_control.can_set_color: self._features |= SUPPORT_COLOR_TEMP
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
@callback @callback
def _observe_update(self, tradfri_device): def _observe_update(self, tradfri_device):
"""Receive new state data for this light.""" """Receive new state data for this light."""
self._refresh(tradfri_device) 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() self.async_schedule_update_ha_state()

View File

@ -38,6 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
'philips.light.ceiling', 'philips.light.ceiling',
'philips.light.zyceiling', 'philips.light.zyceiling',
'philips.light.bulb', 'philips.light.bulb',
'philips.light.candle',
'philips.light.candle2']), 'philips.light.candle2']),
}) })
@ -149,7 +150,9 @@ async def async_setup_platform(hass, config, async_add_devices,
device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id) device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id)
devices.append(device) devices.append(device)
hass.data[DATA_KEY][host] = 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 from miio import PhilipsBulb
light = PhilipsBulb(host, token) light = PhilipsBulb(host, token)
device = XiaomiPhilipsBulb(name, light, model, unique_id) 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_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP,
ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS, ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH, 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.helpers.config_validation as cv
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
@ -30,7 +30,7 @@ DEFAULT_TRANSITION = 350
CONF_SAVE_ON_CHANGE = 'save_on_change' CONF_SAVE_ON_CHANGE = 'save_on_change'
CONF_MODE_MUSIC = 'use_music_mode' CONF_MODE_MUSIC = 'use_music_mode'
DOMAIN = 'yeelight' DATA_KEY = 'light.yeelight'
DEVICE_SCHEMA = vol.Schema({ DEVICE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
@ -90,6 +90,13 @@ YEELIGHT_EFFECT_LIST = [
EFFECT_TWITTER, EFFECT_TWITTER,
EFFECT_STOP] 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): def _cmd(func):
"""Define a wrapper to catch exceptions from the bulb.""" """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): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Yeelight bulbs.""" """Set up the Yeelight bulbs."""
from yeelight.enums import PowerMode
if DATA_KEY not in hass.data:
hass.data[DATA_KEY] = {}
lights = [] lights = []
if discovery_info is not None: if discovery_info is not None:
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname']) _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']) discovery_info['properties']['mac'])
device = {'name': name, 'ipaddr': discovery_info['host']} 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: else:
for ipaddr, device_config in config[CONF_DEVICES].items(): 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} device = {'name': name, 'ipaddr': ipaddr}
lights.append(YeelightLight(device, device_config)) light = YeelightLight(device, device_config)
lights.append(light)
hass.data[DATA_KEY][name] = light
add_devices(lights, True) 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): class YeelightLight(Light):
"""Representation of a Yeelight light.""" """Representation of a Yeelight light."""
@ -444,3 +484,11 @@ class YeelightLight(Light):
self._bulb.turn_off(duration=duration) self._bulb.turn_off(duration=duration)
except yeelight.BulbException as ex: except yeelight.BulbException as ex:
_LOGGER.error("Unable to turn the bulb off: %s", 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 from homeassistant.const import CONF_HOST
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
REQUIREMENTS = ['yeelightsunflower==0.0.8'] REQUIREMENTS = ['yeelightsunflower==0.0.10']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MEDIA_TYPE_CHANNEL, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_EPISODE, 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_VOLUME_MUTE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, DOMAIN, PLATFORM_SCHEMA, SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, DOMAIN, PLATFORM_SCHEMA,
MediaPlayerDevice) MediaPlayerDevice)
@ -281,7 +281,7 @@ class ChannelsPlayer(MediaPlayerDevice):
if media_type == MEDIA_TYPE_CHANNEL: if media_type == MEDIA_TYPE_CHANNEL:
response = self.client.play_channel(media_id) response = self.client.play_channel(media_id)
self.update_state(response) 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]: MEDIA_TYPE_TVSHOW]:
response = self.client.play_recording(media_id) response = self.client.play_recording(media_id)
self.update_state(response) 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/ https://home-assistant.io/components/demo/
""" """
from homeassistant.components.media_player import ( 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_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PLAY,
@ -147,7 +147,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
@property @property
def media_content_type(self): def media_content_type(self):
"""Return the content type of current playing media.""" """Return the content type of current playing media."""
return MEDIA_TYPE_VIDEO return MEDIA_TYPE_MOVIE
@property @property
def media_duration(self): def media_duration(self):

View File

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

View File

@ -10,7 +10,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import ( 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, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA) MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
@ -231,7 +231,7 @@ class EmbyDevice(MediaPlayerDevice):
if media_type == 'Episode': if media_type == 'Episode':
return MEDIA_TYPE_TVSHOW return MEDIA_TYPE_TVSHOW
elif media_type == 'Movie': elif media_type == 'Movie':
return MEDIA_TYPE_VIDEO return MEDIA_TYPE_MOVIE
elif media_type == 'Trailer': elif media_type == 'Trailer':
return MEDIA_TYPE_TRAILER return MEDIA_TYPE_TRAILER
elif media_type == 'Music': 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_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET,
MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_MOVIE, MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL,
MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON) MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON)
from homeassistant.const import ( from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
CONF_PORT, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD,
@ -67,7 +67,7 @@ MEDIA_TYPES = {
'video': MEDIA_TYPE_VIDEO, 'video': MEDIA_TYPE_VIDEO,
'set': MEDIA_TYPE_PLAYLIST, 'set': MEDIA_TYPE_PLAYLIST,
'musicvideo': MEDIA_TYPE_VIDEO, 'musicvideo': MEDIA_TYPE_VIDEO,
'movie': MEDIA_TYPE_VIDEO, 'movie': MEDIA_TYPE_MOVIE,
'tvshow': MEDIA_TYPE_TVSHOW, 'tvshow': MEDIA_TYPE_TVSHOW,
'season': MEDIA_TYPE_TVSHOW, 'season': MEDIA_TYPE_TVSHOW,
'episode': 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.helpers.config_validation as cv
import homeassistant.util.dt as dt_util 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__) _LOGGER = logging.getLogger(__name__)

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ from homeassistant.const import (
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv 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_SONGPAL = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \ SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \
@ -101,7 +101,7 @@ class SongpalDevice(MediaPlayerDevice):
import songpal import songpal
self._name = name self._name = name
self.endpoint = endpoint self.endpoint = endpoint
self.dev = songpal.Protocol(self.endpoint) self.dev = songpal.Device(self.endpoint)
self._sysinfo = None self._sysinfo = None
self._state = False self._state = False

View File

@ -35,6 +35,7 @@ CONF_SOURCES = 'sources'
CONF_ON_ACTION = 'turn_on_action' CONF_ON_ACTION = 'turn_on_action'
DEFAULT_NAME = 'LG webOS Smart TV' DEFAULT_NAME = 'LG webOS Smart TV'
LIVETV_APP_ID = 'com.webos.app.livetv'
WEBOSTV_CONFIG_FILE = 'webostv.conf' WEBOSTV_CONFIG_FILE = 'webostv.conf'
@ -357,8 +358,16 @@ class LgWebOSDevice(MediaPlayerDevice):
def media_next_track(self): def media_next_track(self):
"""Send next track command.""" """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): def media_previous_track(self):
"""Send the previous track command.""" """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