Merge pull request #10898 from home-assistant/release-0-59

0.59
This commit is contained in:
Fabian Affolter 2017-12-03 13:24:22 +01:00 committed by GitHub
commit 850a20a626
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
170 changed files with 5160 additions and 1470 deletions

View File

@ -53,6 +53,8 @@ omit =
homeassistant/components/digital_ocean.py homeassistant/components/digital_ocean.py
homeassistant/components/*/digital_ocean.py homeassistant/components/*/digital_ocean.py
homeassistant/components/dominos.py
homeassistant/components/doorbird.py homeassistant/components/doorbird.py
homeassistant/components/*/doorbird.py homeassistant/components/*/doorbird.py
@ -80,6 +82,9 @@ omit =
homeassistant/components/hdmi_cec.py homeassistant/components/hdmi_cec.py
homeassistant/components/*/hdmi_cec.py homeassistant/components/*/hdmi_cec.py
homeassistant/components/hive.py
homeassistant/components/*/hive.py
homeassistant/components/homematic.py homeassistant/components/homematic.py
homeassistant/components/*/homematic.py homeassistant/components/*/homematic.py
@ -182,6 +187,9 @@ omit =
homeassistant/components/tado.py homeassistant/components/tado.py
homeassistant/components/*/tado.py homeassistant/components/*/tado.py
homeassistant/components/tahoma.py
homeassistant/components/*/tahoma.py
homeassistant/components/tellduslive.py homeassistant/components/tellduslive.py
homeassistant/components/*/tellduslive.py homeassistant/components/*/tellduslive.py
@ -426,7 +434,6 @@ omit =
homeassistant/components/notify/clicksend.py homeassistant/components/notify/clicksend.py
homeassistant/components/notify/clicksend_tts.py homeassistant/components/notify/clicksend_tts.py
homeassistant/components/notify/discord.py homeassistant/components/notify/discord.py
homeassistant/components/notify/facebook.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py homeassistant/components/notify/gntp.py
homeassistant/components/notify/group.py homeassistant/components/notify/group.py
@ -589,7 +596,6 @@ omit =
homeassistant/components/sensor/worldtidesinfo.py homeassistant/components/sensor/worldtidesinfo.py
homeassistant/components/sensor/worxlandroid.py homeassistant/components/sensor/worxlandroid.py
homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yweather.py
homeassistant/components/sensor/zamg.py homeassistant/components/sensor/zamg.py
homeassistant/components/shiftr.py homeassistant/components/shiftr.py
homeassistant/components/spc.py homeassistant/components/spc.py
@ -622,6 +628,7 @@ omit =
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
homeassistant/components/tts/baidu.py
homeassistant/components/tts/microsoft.py homeassistant/components/tts/microsoft.py
homeassistant/components/tts/picotts.py homeassistant/components/tts/picotts.py
homeassistant/components/vacuum/roomba.py homeassistant/components/vacuum/roomba.py
@ -635,7 +642,6 @@ omit =
homeassistant/components/zwave/util.py homeassistant/components/zwave/util.py
homeassistant/components/vacuum/mqtt.py homeassistant/components/vacuum/mqtt.py
[report] [report]
# Regexes for lines to exclude from consideration # Regexes for lines to exclude from consideration
exclude_lines = exclude_lines =

2
.gitignore vendored
View File

@ -96,4 +96,4 @@ docs/build
desktop.ini desktop.ini
/home-assistant.pyproj /home-assistant.pyproj
/home-assistant.sln /home-assistant.sln
/.vs/home-assistant/v14 /.vs/*

View File

@ -8,18 +8,18 @@ matrix:
include: include:
- python: "3.4.2" - python: "3.4.2"
env: TOXENV=lint env: TOXENV=lint
- python: "3.4.2"
env: TOXENV=pylint
- python: "3.4.2" - python: "3.4.2"
env: TOXENV=py34 env: TOXENV=py34
# - python: "3.5" # - python: "3.5"
# env: TOXENV=typing # env: TOXENV=typing
- python: "3.5" - python: "3.5.3"
env: TOXENV=py35 env: TOXENV=py35
- python: "3.6" - python: "3.6"
env: TOXENV=py36 env: TOXENV=py36
# - python: "3.6-dev" # - python: "3.6-dev"
# env: TOXENV=py36 # env: TOXENV=py36
- python: "3.4.2"
env: TOXENV=requirements
# allow_failures: # allow_failures:
# - python: "3.5" # - python: "3.5"
# env: TOXENV=typing # env: TOXENV=typing
@ -29,5 +29,5 @@ cache:
- $HOME/.cache/pip - $HOME/.cache/pip
install: pip install -U tox coveralls install: pip install -U tox coveralls
language: python language: python
script: travis_wait tox script: travis_wait 30 tox --develop
after_success: coveralls after_success: coveralls

View File

@ -46,6 +46,7 @@ homeassistant/components/climate/eq3btsmart.py @rytilahti
homeassistant/components/climate/sensibo.py @andrey-git homeassistant/components/climate/sensibo.py @andrey-git
homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/cover/template.py @PhracturedBlue
homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/automatic.py @armills
homeassistant/components/device_tracker/tile.py @bachya
homeassistant/components/history_graph.py @andrey-git homeassistant/components/history_graph.py @andrey-git
homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/tplink.py @rytilahti
homeassistant/components/light/yeelight.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti
@ -63,13 +64,19 @@ homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/velux.py @Julius2342 homeassistant/components/velux.py @Julius2342
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/tahoma.py @philklei
homeassistant/components/*/tahoma.py @philklei
homeassistant/components/tesla.py @zabuldon homeassistant/components/tesla.py @zabuldon
homeassistant/components/*/tesla.py @zabuldon homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tradfri.py @ggravlingen homeassistant/components/*/tradfri.py @ggravlingen
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -14,7 +14,7 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_NIGHT) SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
@ -33,6 +33,7 @@ SERVICE_TO_METHOD = {
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home', SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away', SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away',
SERVICE_ALARM_ARM_NIGHT: 'alarm_arm_night', SERVICE_ALARM_ARM_NIGHT: 'alarm_arm_night',
SERVICE_ALARM_ARM_CUSTOM_BYPASS: 'alarm_arm_custom_bypass',
SERVICE_ALARM_TRIGGER: 'alarm_trigger' SERVICE_ALARM_TRIGGER: 'alarm_trigger'
} }
@ -107,6 +108,18 @@ def alarm_trigger(hass, code=None, entity_id=None):
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
@bind_hass
def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
"""Send the alarm the command for arm custom bypass."""
data = {}
if code:
data[ATTR_CODE] = code
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data)
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Track states and offer events for sensors.""" """Track states and offer events for sensors."""
@ -216,6 +229,17 @@ class AlarmControlPanel(Entity):
""" """
return self.hass.async_add_job(self.alarm_trigger, code) return self.hass.async_add_job(self.alarm_trigger, code)
def alarm_arm_custom_bypass(self, code=None):
"""Send arm custom bypass command."""
raise NotImplementedError()
def async_alarm_arm_custom_bypass(self, code=None):
"""Send arm custom bypass command.
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_job(self.alarm_arm_custom_bypass, code)
@property @property
def state_attributes(self): def state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""

View File

@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__)
ARMED = 'armed' ARMED = 'armed'
CONF_HOME_MODE_NAME = 'home_mode_name' CONF_HOME_MODE_NAME = 'home_mode_name'
CONF_AWAY_MODE_NAME = 'away_mode_name'
DEPENDENCIES = ['arlo'] DEPENDENCIES = ['arlo']
@ -31,6 +32,7 @@ ICON = 'mdi:security'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string, vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string,
}) })
@ -43,19 +45,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
return return
home_mode_name = config.get(CONF_HOME_MODE_NAME) home_mode_name = config.get(CONF_HOME_MODE_NAME)
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
base_stations = [] base_stations = []
for base_station in data.base_stations: for base_station in data.base_stations:
base_stations.append(ArloBaseStation(base_station, home_mode_name)) base_stations.append(ArloBaseStation(base_station, home_mode_name,
away_mode_name))
async_add_devices(base_stations, True) async_add_devices(base_stations, True)
class ArloBaseStation(AlarmControlPanel): class ArloBaseStation(AlarmControlPanel):
"""Representation of an Arlo Alarm Control Panel.""" """Representation of an Arlo Alarm Control Panel."""
def __init__(self, data, home_mode_name): def __init__(self, data, home_mode_name, away_mode_name):
"""Initialize the alarm control panel.""" """Initialize the alarm control panel."""
self._base_station = data self._base_station = data
self._home_mode_name = home_mode_name self._home_mode_name = home_mode_name
self._away_mode_name = away_mode_name
self._state = None self._state = None
@property @property
@ -89,8 +94,8 @@ class ArloBaseStation(AlarmControlPanel):
@asyncio.coroutine @asyncio.coroutine
def async_alarm_arm_away(self, code=None): def async_alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command. Uses custom mode."""
self._base_station.mode = ARMED self._base_station.mode = self._away_mode_name
@asyncio.coroutine @asyncio.coroutine
def async_alarm_arm_home(self, code=None): def async_alarm_arm_home(self, code=None):
@ -118,4 +123,6 @@ class ArloBaseStation(AlarmControlPanel):
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
elif mode == self._home_mode_name: elif mode == self._home_mode_name:
return STATE_ALARM_ARMED_HOME return STATE_ALARM_ARMED_HOME
elif mode == self._away_mode_name:
return STATE_ALARM_ARMED_AWAY
return None return None

View File

@ -7,7 +7,7 @@ https://home-assistant.io/components/demo/
import homeassistant.components.alarm_control_panel.manual as manual import homeassistant.components.alarm_control_panel.manual as manual
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_TRIGGERED, CONF_PENDING_TIME) STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED, CONF_PENDING_TIME)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -23,6 +23,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
STATE_ALARM_ARMED_NIGHT: { STATE_ALARM_ARMED_NIGHT: {
CONF_PENDING_TIME: 5 CONF_PENDING_TIME: 5
}, },
STATE_ALARM_ARMED_CUSTOM_BYPASS: {
CONF_PENDING_TIME: 5
},
STATE_ALARM_TRIGGERED: { STATE_ALARM_TRIGGERED: {
CONF_PENDING_TIME: 5 CONF_PENDING_TIME: 5
}, },

View File

@ -14,9 +14,9 @@ import homeassistant.components.alarm_control_panel as alarm
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_DISARMED, STATE_ALARM_PENDING,
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME, CONF_CODE,
CONF_DISARM_AFTER_TRIGGER) CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.event import track_point_in_time
@ -26,7 +26,8 @@ DEFAULT_TRIGGER_TIME = 120
DEFAULT_DISARM_AFTER_TRIGGER = False DEFAULT_DISARM_AFTER_TRIGGER = False
SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, SUPPORTED_PENDING_STATES = [STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED] STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED,
STATE_ALARM_ARMED_CUSTOM_BYPASS]
ATTR_POST_PENDING_STATE = 'post_pending_state' ATTR_POST_PENDING_STATE = 'post_pending_state'
@ -59,6 +60,8 @@ PLATFORM_SCHEMA = vol.Schema(vol.All({
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA, vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA, vol.Optional(STATE_ALARM_ARMED_HOME, default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA, vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS,
default={}): STATE_SETTING_SCHEMA,
vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA, vol.Optional(STATE_ALARM_TRIGGERED, default={}): STATE_SETTING_SCHEMA,
}, _state_validator)) }, _state_validator))
@ -174,6 +177,13 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._update_state(STATE_ALARM_ARMED_NIGHT) self._update_state(STATE_ALARM_ARMED_NIGHT)
def alarm_arm_custom_bypass(self, code=None):
"""Send arm custom bypass command."""
if not self._validate_code(code, STATE_ALARM_ARMED_CUSTOM_BYPASS):
return
self._update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS)
def alarm_trigger(self, code=None): def alarm_trigger(self, code=None):
"""Send alarm trigger command. No code needed.""" """Send alarm trigger command. No code needed."""
self._pre_trigger_state = self._state self._pre_trigger_state = self._state

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME) STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME)
REQUIREMENTS = ['total_connect_client==0.13'] REQUIREMENTS = ['total_connect_client==0.16']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -171,7 +171,7 @@ def async_api_discovery(hass, config, request):
# Required description as per Amazon Scene docs # Required description as per Amazon Scene docs
if entity.domain == scene.DOMAIN: if entity.domain == scene.DOMAIN:
scene_fmt = '%s (Scene connected via Home Assistant)' scene_fmt = '{} (Scene connected via Home Assistant)'
description = scene_fmt.format(description) description = scene_fmt.format(description)
cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES

View File

@ -89,6 +89,7 @@ def setup(hass, config):
"""Set up the Amcrest IP Camera component.""" """Set up the Amcrest IP Camera component."""
from amcrest import AmcrestCamera from amcrest import AmcrestCamera
hass.data[DATA_AMCREST] = {}
amcrest_cams = config[DOMAIN] amcrest_cams = config[DOMAIN]
for device in amcrest_cams: for device in amcrest_cams:
@ -126,22 +127,34 @@ def setup(hass, config):
else: else:
authentication = None authentication = None
hass.data[DATA_AMCREST][name] = AmcrestDevice(
camera, name, authentication, ffmpeg_arguments, stream_source,
resolution)
discovery.load_platform( discovery.load_platform(
hass, 'camera', DOMAIN, { hass, 'camera', DOMAIN, {
'device': camera,
CONF_AUTHENTICATION: authentication,
CONF_FFMPEG_ARGUMENTS: ffmpeg_arguments,
CONF_NAME: name, CONF_NAME: name,
CONF_RESOLUTION: resolution,
CONF_STREAM_SOURCE: stream_source,
}, config) }, config)
if sensors: if sensors:
discovery.load_platform( discovery.load_platform(
hass, 'sensor', DOMAIN, { hass, 'sensor', DOMAIN, {
'device': camera,
CONF_NAME: name, CONF_NAME: name,
CONF_SENSORS: sensors, CONF_SENSORS: sensors,
}, config) }, config)
return True return True
class AmcrestDevice(object):
"""Representation of a base Amcrest discovery device."""
def __init__(self, camera, name, authentication, ffmpeg_arguments,
stream_source, resolution):
"""Initialize the entity."""
self.device = camera
self.name = name
self.authentication = authentication
self.ffmpeg_arguments = ffmpeg_arguments
self.stream_source = stream_source
self.resolution = resolution

View File

@ -37,8 +37,8 @@ def async_trigger(hass, config, action):
above = config.get(CONF_ABOVE) above = config.get(CONF_ABOVE)
time_delta = config.get(CONF_FOR) time_delta = config.get(CONF_FOR)
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
async_remove_track_same = None unsub_track_same = {}
already_triggered = False entities_triggered = set()
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
@ -63,8 +63,6 @@ def async_trigger(hass, config, action):
@callback @callback
def state_automation_listener(entity, from_s, to_s): def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
nonlocal already_triggered, async_remove_track_same
@callback @callback
def call_action(): def call_action():
"""Call action with right context.""" """Call action with right context."""
@ -81,16 +79,18 @@ def async_trigger(hass, config, action):
matching = check_numeric_state(entity, from_s, to_s) matching = check_numeric_state(entity, from_s, to_s)
if matching and not already_triggered: if not matching:
entities_triggered.discard(entity)
elif entity not in entities_triggered:
entities_triggered.add(entity)
if time_delta: if time_delta:
async_remove_track_same = async_track_same_state( unsub_track_same[entity] = async_track_same_state(
hass, time_delta, call_action, entity_ids=entity_id, hass, time_delta, call_action, entity_ids=entity_id,
async_check_same_func=check_numeric_state) async_check_same_func=check_numeric_state)
else: else:
call_action() call_action()
already_triggered = matching
unsub = async_track_state_change( unsub = async_track_state_change(
hass, entity_id, state_automation_listener) hass, entity_id, state_automation_listener)
@ -98,7 +98,8 @@ def async_trigger(hass, config, action):
def async_remove(): def async_remove():
"""Remove state listeners async.""" """Remove state listeners async."""
unsub() unsub()
if async_remove_track_same: for async_remove in unsub_track_same.values():
async_remove_track_same() # pylint: disable=not-callable async_remove()
unsub_track_same.clear()
return async_remove return async_remove

View File

@ -35,13 +35,11 @@ def async_trigger(hass, config, action):
to_state = config.get(CONF_TO, MATCH_ALL) to_state = config.get(CONF_TO, MATCH_ALL)
time_delta = config.get(CONF_FOR) time_delta = config.get(CONF_FOR)
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL) match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
async_remove_track_same = None unsub_track_same = {}
@callback @callback
def state_automation_listener(entity, from_s, to_s): def state_automation_listener(entity, from_s, to_s):
"""Listen for state changes and calls action.""" """Listen for state changes and calls action."""
nonlocal async_remove_track_same
@callback @callback
def call_action(): def call_action():
"""Call action with right context.""" """Call action with right context."""
@ -64,7 +62,7 @@ def async_trigger(hass, config, action):
call_action() call_action()
return return
async_remove_track_same = async_track_same_state( unsub_track_same[entity] = async_track_same_state(
hass, time_delta, call_action, hass, time_delta, call_action,
lambda _, _2, to_state: to_state.state == to_s.state, lambda _, _2, to_state: to_state.state == to_s.state,
entity_ids=entity_id) entity_ids=entity_id)
@ -76,7 +74,8 @@ def async_trigger(hass, config, action):
def async_remove(): def async_remove():
"""Remove state listeners async.""" """Remove state listeners async."""
unsub() unsub()
if async_remove_track_same: for async_remove in unsub_track_same.values():
async_remove_track_same() # pylint: disable=not-callable async_remove()
unsub_track_same.clear()
return async_remove return async_remove

View File

@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/axis/ https://home-assistant.io/components/axis/
""" """
import json
import logging import logging
import os import os
@ -22,6 +21,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['axis==14'] REQUIREMENTS = ['axis==14']
@ -103,9 +103,9 @@ def request_configuration(hass, config, name, host, serialnumber):
return False return False
if setup_device(hass, config, device_config): if setup_device(hass, config, device_config):
config_file = _read_config(hass) config_file = load_json(hass.config.path(CONFIG_FILE))
config_file[serialnumber] = dict(device_config) config_file[serialnumber] = dict(device_config)
_write_config(hass, config_file) save_json(hass.config.path(CONFIG_FILE), config_file)
configurator.request_done(request_id) configurator.request_done(request_id)
else: else:
configurator.notify_errors(request_id, configurator.notify_errors(request_id,
@ -163,7 +163,7 @@ def setup(hass, config):
serialnumber = discovery_info['properties']['macaddress'] serialnumber = discovery_info['properties']['macaddress']
if serialnumber not in AXIS_DEVICES: if serialnumber not in AXIS_DEVICES:
config_file = _read_config(hass) config_file = load_json(hass.config.path(CONFIG_FILE))
if serialnumber in config_file: if serialnumber in config_file:
# Device config previously saved to file # Device config previously saved to file
try: try:
@ -274,25 +274,6 @@ def setup_device(hass, config, device_config):
return True return True
def _read_config(hass):
"""Read Axis config."""
path = hass.config.path(CONFIG_FILE)
if not os.path.isfile(path):
return {}
with open(path) as f_handle:
# Guard against empty file
return json.loads(f_handle.read() or '{}')
def _write_config(hass, config):
"""Write Axis config."""
data = json.dumps(config)
with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile:
outfile.write(data)
class AxisDeviceEvent(Entity): class AxisDeviceEvent(Entity):
"""Representation of a Axis device event.""" """Representation of a Axis device event."""

View File

@ -20,6 +20,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEVICE_CLASSES = [ DEVICE_CLASSES = [
'battery', # On means low, Off means normal
'cold', # On means cold (or too cold) 'cold', # On means cold (or too cold)
'connectivity', # On means connection present, Off = no connection 'connectivity', # On means connection present, Off = no connection
'gas', # CO, CO2, etc. 'gas', # CO, CO2, etc.
@ -32,6 +33,7 @@ DEVICE_CLASSES = [
'opening', # Door, window, etc. 'opening', # Door, window, etc.
'plug', # On means plugged in, Off means unplugged 'plug', # On means plugged in, Off means unplugged
'power', # Power, over-current, etc 'power', # Power, over-current, etc
'presence', # On means home, Off means away
'safety', # Generic on=unsafe, off=safe 'safety', # Generic on=unsafe, off=safe
'smoke', # Smoke detector 'smoke', # Smoke detector
'sound', # On means sound detected, Off means no sound 'sound', # On means sound detected, Off means no sound

View File

@ -0,0 +1,63 @@
"""
Support for the Hive devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.hive/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.hive import DATA_HIVE
DEPENDENCIES = ['hive']
DEVICETYPE_DEVICE_CLASS = {'motionsensor': 'motion',
'contactsensor': 'opening'}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Hive sensor devices."""
if discovery_info is None:
return
session = hass.data.get(DATA_HIVE)
add_devices([HiveBinarySensorEntity(session, discovery_info)])
class HiveBinarySensorEntity(BinarySensorDevice):
"""Representation of a Hive binary sensor."""
def __init__(self, hivesession, hivedevice):
"""Initialize the hive sensor."""
self.node_id = hivedevice["Hive_NodeID"]
self.node_name = hivedevice["Hive_NodeName"]
self.device_type = hivedevice["HA_DeviceType"]
self.node_device_type = hivedevice["Hive_DeviceType"]
self.session = hivesession
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
self.session.entities.append(self)
def handle_update(self, updatesource):
"""Handle the new update request."""
if '{}.{}'.format(self.device_type, self.node_id) not in updatesource:
self.schedule_update_ha_state()
@property
def device_class(self):
"""Return the class of this sensor."""
return DEVICETYPE_DEVICE_CLASS.get(self.node_device_type)
@property
def name(self):
"""Return the name of the binary sensor."""
return self.node_name
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self.session.sensor.get_state(self.node_id,
self.node_device_type)
def update(self):
"""Update all Node data frome Hive."""
self.session.core.update_data(self.node_id)

View File

@ -25,6 +25,7 @@ SENSOR_TYPES_CLASS = {
'RemoteMotion': None, 'RemoteMotion': None,
'WeatherSensor': None, 'WeatherSensor': None,
'TiltSensor': None, 'TiltSensor': None,
'PresenceIP': 'motion',
} }

View File

@ -8,9 +8,10 @@ import asyncio
import logging import logging
from homeassistant.components.amcrest import ( from homeassistant.components.amcrest import (
STREAM_SOURCE_LIST, TIMEOUT) DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT)
from homeassistant.components.camera import Camera from homeassistant.components.camera import Camera
from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import CONF_NAME
from homeassistant.helpers.aiohttp_client import ( from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_aiohttp_proxy_web, async_get_clientsession, async_aiohttp_proxy_web,
async_aiohttp_proxy_stream) async_aiohttp_proxy_stream)
@ -26,21 +27,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if discovery_info is None: if discovery_info is None:
return return
device = discovery_info['device'] device_name = discovery_info[CONF_NAME]
authentication = discovery_info['authentication'] amcrest = hass.data[DATA_AMCREST][device_name]
ffmpeg_arguments = discovery_info['ffmpeg_arguments']
name = discovery_info['name']
resolution = discovery_info['resolution']
stream_source = discovery_info['stream_source']
async_add_devices([ async_add_devices([AmcrestCam(hass, amcrest)], True)
AmcrestCam(hass,
name,
device,
authentication,
ffmpeg_arguments,
stream_source,
resolution)], True)
return True return True
@ -48,18 +38,17 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class AmcrestCam(Camera): class AmcrestCam(Camera):
"""An implementation of an Amcrest IP camera.""" """An implementation of an Amcrest IP camera."""
def __init__(self, hass, name, camera, authentication, def __init__(self, hass, amcrest):
ffmpeg_arguments, stream_source, resolution):
"""Initialize an Amcrest camera.""" """Initialize an Amcrest camera."""
super(AmcrestCam, self).__init__() super(AmcrestCam, self).__init__()
self._name = name self._name = amcrest.name
self._camera = camera self._camera = amcrest.device
self._base_url = self._camera.get_base_url() self._base_url = self._camera.get_base_url()
self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = ffmpeg_arguments self._ffmpeg_arguments = amcrest.ffmpeg_arguments
self._stream_source = stream_source self._stream_source = amcrest.stream_source
self._resolution = resolution self._resolution = amcrest.resolution
self._token = self._auth = authentication self._token = self._auth = amcrest.authentication
def camera_image(self): def camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -12,7 +12,8 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.components.ring import DATA_RING, CONF_ATTRIBUTION from homeassistant.components.ring import (
DATA_RING, CONF_ATTRIBUTION, NOTIFICATION_ID)
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import ATTR_ATTRIBUTION, CONF_SCAN_INTERVAL from homeassistant.const import ATTR_ATTRIBUTION, CONF_SCAN_INTERVAL
@ -27,6 +28,8 @@ FORCE_REFRESH_INTERVAL = timedelta(minutes=45)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
NOTIFICATION_TITLE = 'Ring Camera Setup'
SCAN_INTERVAL = timedelta(seconds=90) SCAN_INTERVAL = timedelta(seconds=90)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -42,11 +45,33 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
ring = hass.data[DATA_RING] ring = hass.data[DATA_RING]
cams = [] cams = []
cams_no_plan = []
for camera in ring.doorbells: for camera in ring.doorbells:
cams.append(RingCam(hass, camera, config)) if camera.has_subscription:
cams.append(RingCam(hass, camera, config))
else:
cams_no_plan.append(camera)
for camera in ring.stickup_cams: for camera in ring.stickup_cams:
cams.append(RingCam(hass, camera, config)) if camera.has_subscription:
cams.append(RingCam(hass, camera, config))
else:
cams_no_plan.append(camera)
# show notification for all cameras without an active subscription
if cams_no_plan:
cameras = str(', '.join([camera.name for camera in cams_no_plan]))
err_msg = '''A Ring Protect Plan is required for the''' \
''' following cameras: {}.'''.format(cameras)
_LOGGER.error(err_msg)
hass.components.persistent_notification.async_create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(err_msg),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
async_add_devices(cams, True) async_add_devices(cams, True)
return True return True
@ -84,7 +109,6 @@ class RingCam(Camera):
'timezone': self._camera.timezone, 'timezone': self._camera.timezone,
'type': self._camera.family, 'type': self._camera.family,
'video_url': self._video_url, 'video_url': self._video_url,
'video_id': self._last_video_id
} }
@asyncio.coroutine @asyncio.coroutine

View File

@ -51,6 +51,19 @@ STATE_HIGH_DEMAND = 'high_demand'
STATE_HEAT_PUMP = 'heat_pump' STATE_HEAT_PUMP = 'heat_pump'
STATE_GAS = 'gas' STATE_GAS = 'gas'
SUPPORT_TARGET_TEMPERATURE = 1
SUPPORT_TARGET_TEMPERATURE_HIGH = 2
SUPPORT_TARGET_TEMPERATURE_LOW = 4
SUPPORT_TARGET_HUMIDITY = 8
SUPPORT_TARGET_HUMIDITY_HIGH = 16
SUPPORT_TARGET_HUMIDITY_LOW = 32
SUPPORT_FAN_MODE = 64
SUPPORT_OPERATION_MODE = 128
SUPPORT_HOLD_MODE = 256
SUPPORT_SWING_MODE = 512
SUPPORT_AWAY_MODE = 1024
SUPPORT_AUX_HEAT = 2048
ATTR_CURRENT_TEMPERATURE = 'current_temperature' ATTR_CURRENT_TEMPERATURE = 'current_temperature'
ATTR_MAX_TEMP = 'max_temp' ATTR_MAX_TEMP = 'max_temp'
ATTR_MIN_TEMP = 'min_temp' ATTR_MIN_TEMP = 'min_temp'
@ -717,6 +730,11 @@ class ClimateDevice(Entity):
""" """
return self.hass.async_add_job(self.turn_aux_heat_off) return self.hass.async_add_job(self.turn_aux_heat_off)
@property
def supported_features(self):
"""Return the list of supported features."""
raise NotImplementedError()
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""

View File

@ -5,9 +5,19 @@ 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.climate import ( from homeassistant.components.climate import (
ClimateDevice, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW) ClimateDevice, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY,
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_AUX_HEAT, SUPPORT_SWING_MODE,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY |
SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE |
SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT |
SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo climate devices.""" """Set up the Demo climate devices."""
@ -47,6 +57,11 @@ class DemoClimate(ClimateDevice):
self._target_temperature_high = target_temp_high self._target_temperature_high = target_temp_high
self._target_temperature_low = target_temp_low self._target_temperature_low = target_temp_low
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def should_poll(self): def should_poll(self):
"""Return the polling state.""" """Return the polling state."""

View File

@ -12,7 +12,9 @@ import voluptuous as vol
from homeassistant.components import ecobee from homeassistant.components import ecobee
from homeassistant.components.climate import ( from homeassistant.components.climate import (
DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice, DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH) ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
@ -44,6 +46,10 @@ RESUME_PROGRAM_SCHEMA = vol.Schema({
vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean, vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean,
}) })
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Ecobee Thermostat Platform.""" """Set up the Ecobee Thermostat Platform."""
@ -132,6 +138,11 @@ class Thermostat(ClimateDevice):
self.thermostat = self.data.ecobee.get_thermostat( self.thermostat = self.data.ecobee.get_thermostat(
self.thermostat_index) self.thermostat_index)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def name(self): def name(self):
"""Return the name of the Ecobee Thermostat.""" """Return the name of the Ecobee Thermostat."""
@ -318,8 +329,21 @@ class Thermostat(ClimateDevice):
def set_auto_temp_hold(self, heat_temp, cool_temp): def set_auto_temp_hold(self, heat_temp, cool_temp):
"""Set temperature hold in auto mode.""" """Set temperature hold in auto mode."""
self.data.ecobee.set_hold_temp(self.thermostat_index, cool_temp, if cool_temp is not None:
heat_temp, self.hold_preference()) cool_temp_setpoint = cool_temp
else:
cool_temp_setpoint = (
self.thermostat['runtime']['desiredCool'] / 10.0)
if heat_temp is not None:
heat_temp_setpoint = heat_temp
else:
heat_temp_setpoint = (
self.thermostat['runtime']['desiredCool'] / 10.0)
self.data.ecobee.set_hold_temp(self.thermostat_index,
cool_temp_setpoint, heat_temp_setpoint,
self.hold_preference())
_LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, " _LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, "
"cool=%s, is=%s", heat_temp, isinstance( "cool=%s, is=%s", heat_temp, isinstance(
heat_temp, (int, float)), cool_temp, heat_temp, (int, float)), cool_temp,
@ -348,8 +372,8 @@ class Thermostat(ClimateDevice):
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
if self.current_operation == STATE_AUTO and low_temp is not None \ if self.current_operation == STATE_AUTO and (low_temp is not None or
and high_temp is not None: high_temp is not None):
self.set_auto_temp_hold(low_temp, high_temp) self.set_auto_temp_hold(low_temp, high_temp)
elif temp is not None: elif temp is not None:
self.set_temp_hold(temp) self.set_temp_hold(temp)
@ -357,6 +381,10 @@ class Thermostat(ClimateDevice):
_LOGGER.error( _LOGGER.error(
"Missing valid arguments for set_temperature in %s", kwargs) "Missing valid arguments for set_temperature in %s", kwargs)
def set_humidity(self, humidity):
"""Set the humidity level."""
self.data.ecobee.set_humidity(self.thermostat_index, humidity)
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" """Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
self.data.ecobee.set_hvac_mode(self.thermostat_index, operation_mode) self.data.ecobee.set_hvac_mode(self.thermostat_index, operation_mode)

View File

@ -9,7 +9,7 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE) ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, SUPPORT_AUX_HEAT)
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD) TEMP_CELSIUS, CONF_USERNAME, CONF_PASSWORD)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -56,6 +56,11 @@ class EphEmberThermostat(ClimateDevice):
self._zone = zone self._zone = zone
self._hot_water = zone['isHotWater'] self._hot_water = zone['isHotWater']
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_AUX_HEAT
@property @property
def name(self): def name(self):
"""Return the name of the thermostat, if any.""" """Return the name of the thermostat, if any."""

View File

@ -9,7 +9,8 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice) STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE)
from homeassistant.const import ( from homeassistant.const import (
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -37,6 +38,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Schema({cv.string: DEVICE_SCHEMA}), vol.Schema({cv.string: DEVICE_SCHEMA}),
}) })
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the eQ-3 BLE thermostats.""" """Set up the eQ-3 BLE thermostats."""
@ -72,6 +76,11 @@ class EQ3BTSmartThermostat(ClimateDevice):
self._name = _name self._name = _name
self._thermostat = eq3.Thermostat(_mac) self._thermostat = eq3.Thermostat(_mac)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return if thermostat is available.""" """Return if thermostat is available."""

View File

@ -17,7 +17,9 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_SLAVE, TEMP_CELSIUS, CONF_NAME, CONF_SLAVE, TEMP_CELSIUS,
ATTR_TEMPERATURE, DEVICE_DEFAULT_NAME) ATTR_TEMPERATURE, DEVICE_DEFAULT_NAME)
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA) from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_FAN_MODE)
import homeassistant.components.modbus as modbus import homeassistant.components.modbus as modbus
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -31,6 +33,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Flexit Platform.""" """Set up the Flexit Platform."""
@ -62,6 +66,11 @@ class Flexit(ClimateDevice):
self._alarm = False self._alarm = False
self.unit = pyflexit.pyflexit(modbus.HUB, modbus_slave) self.unit = pyflexit.pyflexit(modbus.HUB, modbus_slave)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
def update(self): def update(self):
"""Update unit attributes.""" """Update unit attributes."""
if not self.unit.update(): if not self.unit.update():

View File

@ -10,17 +10,18 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components import switch from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_COOL, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
STATE_AUTO) STATE_AUTO, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
CONF_NAME) CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
from homeassistant.helpers import condition from homeassistant.helpers import condition
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change, async_track_time_interval) async_track_state_change, async_track_time_interval)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.restore_state import async_get_last_state
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -40,6 +41,7 @@ CONF_COLD_TOLERANCE = 'cold_tolerance'
CONF_HOT_TOLERANCE = 'hot_tolerance' CONF_HOT_TOLERANCE = 'hot_tolerance'
CONF_KEEP_ALIVE = 'keep_alive' CONF_KEEP_ALIVE = 'keep_alive'
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HEATER): cv.entity_id, vol.Required(CONF_HEATER): cv.entity_id,
@ -117,6 +119,17 @@ class GenericThermostat(ClimateDevice):
if sensor_state: if sensor_state:
self._async_update_temp(sensor_state) self._async_update_temp(sensor_state)
@asyncio.coroutine
def async_added_to_hass(self):
"""Run when entity about to be added."""
# If we have an old state and no target temp, restore
if self._target_temp is None:
old_state = yield from async_get_last_state(self.hass,
self.entity_id)
if old_state is not None:
self._target_temp = float(
old_state.attributes[ATTR_TEMPERATURE])
@property @property
def should_poll(self): def should_poll(self):
"""Return the polling state.""" """Return the polling state."""
@ -167,7 +180,7 @@ class GenericThermostat(ClimateDevice):
elif operation_mode == STATE_OFF: elif operation_mode == STATE_OFF:
self._enabled = False self._enabled = False
if self._is_device_active: if self._is_device_active:
switch.async_turn_off(self.hass, self.heater_entity_id) self._heater_turn_off()
else: else:
_LOGGER.error('Unrecognized operation mode: %s', operation_mode) _LOGGER.error('Unrecognized operation mode: %s', operation_mode)
return return
@ -225,9 +238,9 @@ class GenericThermostat(ClimateDevice):
def _async_keep_alive(self, time): def _async_keep_alive(self, time):
"""Call at constant intervals for keep-alive purposes.""" """Call at constant intervals for keep-alive purposes."""
if self.current_operation in [STATE_COOL, STATE_HEAT]: if self.current_operation in [STATE_COOL, STATE_HEAT]:
switch.async_turn_on(self.hass, self.heater_entity_id) self._heater_turn_on()
else: else:
switch.async_turn_off(self.hass, self.heater_entity_id) self._heater_turn_off()
@callback @callback
def _async_update_temp(self, state): def _async_update_temp(self, state):
@ -273,13 +286,13 @@ class GenericThermostat(ClimateDevice):
self._cold_tolerance self._cold_tolerance
if too_cold: if too_cold:
_LOGGER.info('Turning off AC %s', self.heater_entity_id) _LOGGER.info('Turning off AC %s', self.heater_entity_id)
switch.async_turn_off(self.hass, self.heater_entity_id) self._heater_turn_off()
else: else:
too_hot = self._cur_temp - self._target_temp >= \ too_hot = self._cur_temp - self._target_temp >= \
self._hot_tolerance self._hot_tolerance
if too_hot: if too_hot:
_LOGGER.info('Turning on AC %s', self.heater_entity_id) _LOGGER.info('Turning on AC %s', self.heater_entity_id)
switch.async_turn_on(self.hass, self.heater_entity_id) self._heater_turn_on()
else: else:
is_heating = self._is_device_active is_heating = self._is_device_active
if is_heating: if is_heating:
@ -288,15 +301,34 @@ class GenericThermostat(ClimateDevice):
if too_hot: if too_hot:
_LOGGER.info('Turning off heater %s', _LOGGER.info('Turning off heater %s',
self.heater_entity_id) self.heater_entity_id)
switch.async_turn_off(self.hass, self.heater_entity_id) self._heater_turn_off()
else: else:
too_cold = self._target_temp - self._cur_temp >= \ too_cold = self._target_temp - self._cur_temp >= \
self._cold_tolerance self._cold_tolerance
if too_cold: if too_cold:
_LOGGER.info('Turning on heater %s', self.heater_entity_id) _LOGGER.info('Turning on heater %s', self.heater_entity_id)
switch.async_turn_on(self.hass, self.heater_entity_id) self._heater_turn_on()
@property @property
def _is_device_active(self): def _is_device_active(self):
"""If the toggleable device is currently active.""" """If the toggleable device is currently active."""
return switch.is_on(self.hass, self.heater_entity_id) return self.hass.states.is_state(self.heater_entity_id, STATE_ON)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@callback
def _heater_turn_on(self):
"""Turn heater toggleable device on."""
data = {ATTR_ENTITY_ID: self.heater_entity_id}
self.hass.async_add_job(
self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_ON, data))
@callback
def _heater_turn_off(self):
"""Turn heater toggleable device off."""
data = {ATTR_ENTITY_ID: self.heater_entity_id}
self.hass.async_add_job(
self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data))

View File

@ -8,7 +8,8 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_PORT, CONF_NAME, CONF_ID) TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_PORT, CONF_NAME, CONF_ID)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -68,6 +69,11 @@ class HeatmiserV3Thermostat(ClimateDevice):
self.update() self.update()
self._target_temperature = int(self.dcb.get('roomset')) self._target_temperature = int(self.dcb.get('roomset'))
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_TARGET_TEMPERATURE
@property @property
def name(self): def name(self):
"""Return the name of the thermostat, if any.""" """Return the name of the thermostat, if any."""

View File

@ -0,0 +1,139 @@
"""
Support for the Hive devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.hive/
"""
from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.components.hive import DATA_HIVE
DEPENDENCIES = ['hive']
HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT,
'ON': STATE_ON, 'OFF': STATE_OFF}
HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL',
STATE_ON: 'ON', STATE_OFF: 'OFF'}
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Hive climate devices."""
if discovery_info is None:
return
session = hass.data.get(DATA_HIVE)
add_devices([HiveClimateEntity(session, discovery_info)])
class HiveClimateEntity(ClimateDevice):
"""Hive Climate Device."""
def __init__(self, hivesession, hivedevice):
"""Initialize the Climate device."""
self.node_id = hivedevice["Hive_NodeID"]
self.node_name = hivedevice["Hive_NodeName"]
self.device_type = hivedevice["HA_DeviceType"]
self.session = hivesession
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
if self.device_type == "Heating":
self.modes = [STATE_AUTO, STATE_HEAT, STATE_OFF]
elif self.device_type == "HotWater":
self.modes = [STATE_AUTO, STATE_ON, STATE_OFF]
self.session.entities.append(self)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
def handle_update(self, updatesource):
"""Handle the new update request."""
if '{}.{}'.format(self.device_type, self.node_id) not in updatesource:
self.schedule_update_ha_state()
@property
def name(self):
"""Return the name of the Climate device."""
friendly_name = "Climate Device"
if self.device_type == "Heating":
friendly_name = "Heating"
if self.node_name is not None:
friendly_name = '{} {}'.format(self.node_name, friendly_name)
elif self.device_type == "HotWater":
friendly_name = "Hot Water"
return friendly_name
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
if self.device_type == "Heating":
return self.session.heating.current_temperature(self.node_id)
@property
def target_temperature(self):
"""Return the target temperature."""
if self.device_type == "Heating":
return self.session.heating.get_target_temperature(self.node_id)
@property
def min_temp(self):
"""Return minimum temperature."""
if self.device_type == "Heating":
return self.session.heating.min_temperature(self.node_id)
@property
def max_temp(self):
"""Return the maximum temperature."""
if self.device_type == "Heating":
return self.session.heating.max_temperature(self.node_id)
@property
def operation_list(self):
"""List of the operation modes."""
return self.modes
@property
def current_operation(self):
"""Return current mode."""
if self.device_type == "Heating":
currentmode = self.session.heating.get_mode(self.node_id)
elif self.device_type == "HotWater":
currentmode = self.session.hotwater.get_mode(self.node_id)
return HIVE_TO_HASS_STATE.get(currentmode)
def set_operation_mode(self, operation_mode):
"""Set new Heating mode."""
new_mode = HASS_TO_HIVE_STATE.get(operation_mode)
if self.device_type == "Heating":
self.session.heating.set_mode(self.node_id, new_mode)
elif self.device_type == "HotWater":
self.session.hotwater.set_mode(self.node_id, new_mode)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
def set_temperature(self, **kwargs):
"""Set new target temperature."""
new_temperature = kwargs.get(ATTR_TEMPERATURE)
if new_temperature is not None:
if self.device_type == "Heating":
self.session.heating.set_target_temperature(self.node_id,
new_temperature)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
def update(self):
"""Update all Node data frome Hive."""
self.session.core.update_data(self.node_id)

View File

@ -5,7 +5,9 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.homematic/ https://home-assistant.io/components/climate.homematic/
""" """
import logging import logging
from homeassistant.components.climate import ClimateDevice, STATE_AUTO from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE)
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
@ -38,6 +40,8 @@ HM_HUMI_MAP = [
HM_CONTROL_MODE = 'CONTROL_MODE' HM_CONTROL_MODE = 'CONTROL_MODE'
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Homematic thermostat platform.""" """Set up the Homematic thermostat platform."""
@ -55,6 +59,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class HMThermostat(HMDevice, ClimateDevice): class HMThermostat(HMDevice, ClimateDevice):
"""Representation of a Homematic thermostat.""" """Representation of a Homematic thermostat."""
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement that is used.""" """Return the unit of measurement that is used."""

View File

@ -14,12 +14,13 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, ATTR_FAN_MODE, ATTR_FAN_LIST, ClimateDevice, PLATFORM_SCHEMA, ATTR_FAN_MODE, ATTR_FAN_LIST,
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST) ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
ATTR_TEMPERATURE, CONF_REGION) ATTR_TEMPERATURE, CONF_REGION)
REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.4.1'] REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -126,6 +127,14 @@ class RoundThermostat(ClimateDevice):
self._away_temp = away_temp self._away_temp = away_temp
self._away = False self._away = False
@property
def supported_features(self):
"""Return the list of supported features."""
supported = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE)
if hasattr(self.client, ATTR_SYSTEM_MODE):
supported |= SUPPORT_OPERATION_MODE
return supported
@property @property
def name(self): def name(self):
"""Return the name of the honeywell, if any.""" """Return the name of the honeywell, if any."""
@ -234,6 +243,14 @@ class HoneywellUSThermostat(ClimateDevice):
self._username = username self._username = username
self._password = password self._password = password
@property
def supported_features(self):
"""Return the list of supported features."""
supported = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE)
if hasattr(self._device, ATTR_SYSTEM_MODE):
supported |= SUPPORT_OPERATION_MODE
return supported
@property @property
def is_fan_on(self): def is_fan_on(self):
"""Return true if fan is on.""" """Return true if fan is on."""

View File

@ -8,7 +8,9 @@ import asyncio
import voluptuous as vol import voluptuous as vol
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate import (
PLATFORM_SCHEMA, ClimateDevice, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE)
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE from homeassistant.const import CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -135,6 +137,14 @@ class KNXClimate(ClimateDevice):
self._unit_of_measurement = TEMP_CELSIUS self._unit_of_measurement = TEMP_CELSIUS
@property
def supported_features(self):
"""Return the list of supported features."""
support = SUPPORT_TARGET_TEMPERATURE
if self.device.supports_operation_mode:
support |= SUPPORT_OPERATION_MODE
return support
def async_register_callbacks(self): def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed.""" """Register callbacks to update hass after device was changed."""
@asyncio.coroutine @asyncio.coroutine

View File

@ -7,7 +7,9 @@ https://home-assistant.io/components/maxcube/
import socket import socket
import logging import logging
from homeassistant.components.climate import ClimateDevice, STATE_AUTO from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE)
from homeassistant.components.maxcube import MAXCUBE_HANDLE from homeassistant.components.maxcube import MAXCUBE_HANDLE
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
@ -17,6 +19,8 @@ STATE_MANUAL = 'manual'
STATE_BOOST = 'boost' STATE_BOOST = 'boost'
STATE_VACATION = 'vacation' STATE_VACATION = 'vacation'
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Iterate through all MAX! Devices and add thermostats.""" """Iterate through all MAX! Devices and add thermostats."""
@ -47,6 +51,11 @@ class MaxCubeClimate(ClimateDevice):
self._rf_address = rf_address self._rf_address = rf_address
self._cubehandle = hass.data[MAXCUBE_HANDLE] self._cubehandle = hass.data[MAXCUBE_HANDLE]
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def should_poll(self): def should_poll(self):
"""Return the polling state.""" """Return the polling state."""

View File

@ -15,7 +15,9 @@ import homeassistant.components.mqtt as mqtt
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice, STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice,
PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO, PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO,
ATTR_OPERATION_MODE) ATTR_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE,
SUPPORT_AUX_HEAT)
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME) STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME)
from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN, from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN,
@ -483,3 +485,38 @@ class MqttClimate(ClimateDevice):
if self._topic[CONF_AUX_STATE_TOPIC] is None: if self._topic[CONF_AUX_STATE_TOPIC] is None:
self._aux = False self._aux = False
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@property
def supported_features(self):
"""Return the list of supported features."""
support = 0
if (self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None) or \
(self._topic[CONF_TEMPERATURE_COMMAND_TOPIC] is not None):
support |= SUPPORT_TARGET_TEMPERATURE
if (self._topic[CONF_MODE_COMMAND_TOPIC] is not None) or \
(self._topic[CONF_MODE_STATE_TOPIC] is not None):
support |= SUPPORT_OPERATION_MODE
if (self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None) or \
(self._topic[CONF_FAN_MODE_COMMAND_TOPIC] is not None):
support |= SUPPORT_FAN_MODE
if (self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None) or \
(self._topic[CONF_SWING_MODE_COMMAND_TOPIC] is not None):
support |= SUPPORT_SWING_MODE
if (self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None) or \
(self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None):
support |= SUPPORT_AWAY_MODE
if (self._topic[CONF_HOLD_STATE_TOPIC] is not None) or \
(self._topic[CONF_HOLD_COMMAND_TOPIC] is not None):
support |= SUPPORT_HOLD_MODE
if (self._topic[CONF_AUX_STATE_TOPIC] is not None) or \
(self._topic[CONF_AUX_COMMAND_TOPIC] is not None):
support |= SUPPORT_AUX_HEAT
return support

View File

@ -7,7 +7,9 @@ https://home-assistant.io/components/climate.mysensors/
from homeassistant.components import mysensors from homeassistant.components import mysensors
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO,
STATE_COOL, STATE_HEAT, STATE_OFF, ClimateDevice) STATE_COOL, STATE_HEAT, STATE_OFF, ClimateDevice,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH,
SUPPORT_TARGET_TEMPERATURE_LOW, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
DICT_HA_TO_MYS = { DICT_HA_TO_MYS = {
@ -23,6 +25,10 @@ DICT_MYS_TO_HA = {
'Off': STATE_OFF, 'Off': STATE_OFF,
} }
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE |
SUPPORT_OPERATION_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the mysensors climate.""" """Setup the mysensors climate."""
@ -33,6 +39,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
"""Representation of a MySensors HVAC.""" """Representation of a MySensors HVAC."""
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def assumed_state(self): def assumed_state(self):
"""Return True if unable to access real state of entity.""" """Return True if unable to access real state of entity."""

View File

@ -12,7 +12,9 @@ from homeassistant.components.nest import DATA_NEST
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
ATTR_TEMPERATURE) ATTR_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE)
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN) CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
@ -28,6 +30,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
STATE_ECO = 'eco' STATE_ECO = 'eco'
STATE_HEAT_COOL = 'heat-cool' STATE_HEAT_COOL = 'heat-cool'
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE | SUPPORT_FAN_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Nest thermostat.""" """Set up the Nest thermostat."""
@ -87,6 +93,11 @@ class NestThermostat(ClimateDevice):
self._min_temperature = None self._min_temperature = None
self._max_temperature = None self._max_temperature = None
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def name(self): def name(self):
"""Return the name of the nest, if any.""" """Return the name of the nest, if any."""

View File

@ -10,7 +10,8 @@ import voluptuous as vol
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA) STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE)
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.loader import get_component from homeassistant.loader import get_component
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -35,6 +36,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All(cv.ensure_list, [cv.string]), vol.All(cv.ensure_list, [cv.string]),
}) })
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the NetAtmo Thermostat.""" """Set up the NetAtmo Thermostat."""
@ -65,6 +69,11 @@ class NetatmoThermostat(ClimateDevice):
self._target_temperature = None self._target_temperature = None
self._away = None self._away = None
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""

View File

@ -14,7 +14,8 @@ import voluptuous as vol
# Import the device class from the component that you want to support # Import the device class from the component that you want to support
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, ATTR_TEMPERATURE) ClimateDevice, PLATFORM_SCHEMA, STATE_HEAT, STATE_IDLE, ATTR_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE)
from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
CONF_PORT, TEMP_CELSIUS, CONF_NAME) CONF_PORT, TEMP_CELSIUS, CONF_NAME)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -34,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_AWAY_TEMP, default=14): vol.Coerce(float) vol.Optional(CONF_AWAY_TEMP, default=14): vol.Coerce(float)
}) })
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the oemthermostat platform.""" """Set up the oemthermostat platform."""
@ -77,6 +80,11 @@ class ThermostatDevice(ClimateDevice):
self._temperature = None self._temperature = None
self._setpoint = None self._setpoint = None
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def name(self): def name(self):
"""Return the name of this Thermostat.""" """Return the name of this Thermostat."""

View File

@ -8,7 +8,7 @@ import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
PRECISION_TENTHS, STATE_COOL, STATE_HEAT, STATE_IDLE, PRECISION_TENTHS, STATE_COOL, STATE_HEAT, STATE_IDLE,
ClimateDevice, PLATFORM_SCHEMA) ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -46,6 +46,11 @@ class ProliphixThermostat(ClimateDevice):
self._pdp.update() self._pdp.update()
self._name = self._pdp.name self._name = self._pdp.name
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_TARGET_TEMPERATURE
@property @property
def should_poll(self): def should_poll(self):
"""Set up polling needed for thermostat.""" """Set up polling needed for thermostat."""

View File

@ -4,15 +4,18 @@ Support for Radio Thermostat wifi-enabled home thermostats.
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/climate.radiotherm/ https://home-assistant.io/components/climate.radiotherm/
""" """
import asyncio
import datetime import datetime
import logging import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_OFF, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_IDLE, STATE_ON, STATE_OFF,
ClimateDevice, PLATFORM_SCHEMA) ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
from homeassistant.const import CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE)
from homeassistant.const import (
CONF_HOST, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, PRECISION_HALVES)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['radiotherm==1.3'] REQUIREMENTS = ['radiotherm==1.3']
@ -29,15 +32,56 @@ CONF_AWAY_TEMPERATURE_COOL = 'away_temperature_cool'
DEFAULT_AWAY_TEMPERATURE_HEAT = 60 DEFAULT_AWAY_TEMPERATURE_HEAT = 60
DEFAULT_AWAY_TEMPERATURE_COOL = 85 DEFAULT_AWAY_TEMPERATURE_COOL = 85
STATE_CIRCULATE = "circulate"
OPERATION_LIST = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
CT30_FAN_OPERATION_LIST = [STATE_ON, STATE_AUTO]
CT80_FAN_OPERATION_LIST = [STATE_ON, STATE_CIRCULATE, STATE_AUTO]
# Mappings from radiotherm json data codes to and from HASS state
# flags. CODE is the thermostat integer code and these map to and
# from HASS state flags.
# Programmed temperature mode of the thermostat.
CODE_TO_TEMP_MODE = {0: STATE_OFF, 1: STATE_HEAT, 2: STATE_COOL, 3: STATE_AUTO}
TEMP_MODE_TO_CODE = {v: k for k, v in CODE_TO_TEMP_MODE.items()}
# Programmed fan mode (circulate is supported by CT80 models)
CODE_TO_FAN_MODE = {0: STATE_AUTO, 1: STATE_CIRCULATE, 2: STATE_ON}
FAN_MODE_TO_CODE = {v: k for k, v in CODE_TO_FAN_MODE.items()}
# Active thermostat state (is it heating or cooling?). In the future
# this should probably made into heat and cool binary sensors.
CODE_TO_TEMP_STATE = {0: STATE_IDLE, 1: STATE_HEAT, 2: STATE_COOL}
# Active fan state. This is if the fan is actually on or not. In the
# future this should probably made into a binary sensor for the fan.
CODE_TO_FAN_STATE = {0: STATE_OFF, 1: STATE_ON}
def round_temp(temperature):
"""Round a temperature to the resolution of the thermostat.
RadioThermostats can handle 0.5 degree temps so the input
temperature is rounded to that value and returned.
"""
return round(temperature * 2.0) / 2.0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean, vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean,
vol.Optional(CONF_AWAY_TEMPERATURE_HEAT, vol.Optional(CONF_AWAY_TEMPERATURE_HEAT,
default=DEFAULT_AWAY_TEMPERATURE_HEAT): vol.Coerce(float), default=DEFAULT_AWAY_TEMPERATURE_HEAT):
vol.All(vol.Coerce(float), round_temp),
vol.Optional(CONF_AWAY_TEMPERATURE_COOL, vol.Optional(CONF_AWAY_TEMPERATURE_COOL,
default=DEFAULT_AWAY_TEMPERATURE_COOL): vol.Coerce(float), default=DEFAULT_AWAY_TEMPERATURE_COOL):
vol.All(vol.Coerce(float), round_temp),
}) })
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Radio Thermostat.""" """Set up the Radio Thermostat."""
@ -77,19 +121,39 @@ class RadioThermostat(ClimateDevice):
def __init__(self, device, hold_temp, away_temps): def __init__(self, device, hold_temp, away_temps):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self.device = device self.device = device
self.set_time()
self._target_temperature = None self._target_temperature = None
self._current_temperature = None self._current_temperature = None
self._current_operation = STATE_IDLE self._current_operation = STATE_IDLE
self._name = None self._name = None
self._fmode = None self._fmode = None
self._fstate = None
self._tmode = None self._tmode = None
self._tstate = None self._tstate = None
self._hold_temp = hold_temp self._hold_temp = hold_temp
self._hold_set = False
self._away = False self._away = False
self._away_temps = away_temps self._away_temps = away_temps
self._prev_temp = None self._prev_temp = None
self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
# Fan circulate mode is only supported by the CT80 models.
import radiotherm
self._is_model_ct80 = isinstance(self.device,
radiotherm.thermostat.CT80)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
# Set the time on the device. This shouldn't be in the
# constructor because it's a network call. We can't put it in
# update() because calling it will clear any temporary mode or
# temperature in the thermostat. So add it as a future job
# for the event loop to run.
self.hass.async_add_job(self.set_time)
@property @property
def name(self): def name(self):
@ -101,6 +165,11 @@ class RadioThermostat(ClimateDevice):
"""Return the unit of measurement.""" """Return the unit of measurement."""
return TEMP_FAHRENHEIT return TEMP_FAHRENHEIT
@property
def precision(self):
"""Return the precision of the system."""
return PRECISION_HALVES
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device specific state attributes.""" """Return the device specific state attributes."""
@ -109,6 +178,25 @@ class RadioThermostat(ClimateDevice):
ATTR_MODE: self._tmode, ATTR_MODE: self._tmode,
} }
@property
def fan_list(self):
"""List of available fan modes."""
if self._is_model_ct80:
return CT80_FAN_OPERATION_LIST
else:
return CT30_FAN_OPERATION_LIST
@property
def current_fan_mode(self):
"""Return whether the fan is on."""
return self._fmode
def set_fan_mode(self, fan):
"""Turn fan on/off."""
code = FAN_MODE_TO_CODE.get(fan, None)
if code is not None:
self.device.fmode = code
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
@ -122,7 +210,7 @@ class RadioThermostat(ClimateDevice):
@property @property
def operation_list(self): def operation_list(self):
"""Return the operation modes list.""" """Return the operation modes list."""
return self._operation_list return OPERATION_LIST
@property @property
def target_temperature(self): def target_temperature(self):
@ -136,53 +224,48 @@ class RadioThermostat(ClimateDevice):
def update(self): def update(self):
"""Update and validate the data from the thermostat.""" """Update and validate the data from the thermostat."""
current_temp = self.device.temp['raw'] # Radio thermostats are very slow, and sometimes don't respond
if current_temp == -1: # very quickly. So we need to keep the number of calls to them
_LOGGER.error("Couldn't get valid temperature reading") # to a bare minimum or we'll hit the HASS 10 sec warning. We
return # have to make one call to /tstat to get temps but we'll try and
self._current_temperature = current_temp # keep the other calls to a minimum. Even with this, these
self._name = self.device.name['raw'] # thermostats tend to time out sometimes when they're actively
try: # heating or cooling.
self._fmode = self.device.fmode['human']
except AttributeError:
_LOGGER.error("Couldn't get valid fan mode reading")
try:
self._tmode = self.device.tmode['human']
except AttributeError:
_LOGGER.error("Couldn't get valid thermostat mode reading")
try:
self._tstate = self.device.tstate['human']
except AttributeError:
_LOGGER.error("Couldn't get valid thermostat state reading")
if self._tmode == 'Cool': # First time - get the name from the thermostat. This is
target_temp = self.device.t_cool['raw'] # normally set in the radio thermostat web app.
if target_temp == -1: if self._name is None:
_LOGGER.error("Couldn't get target reading") self._name = self.device.name['raw']
return
self._target_temperature = target_temp # Request the current state from the thermostat.
self._current_operation = STATE_COOL data = self.device.tstat['raw']
elif self._tmode == 'Heat':
target_temp = self.device.t_heat['raw'] current_temp = data['temp']
if target_temp == -1: if current_temp == -1:
_LOGGER.error("Couldn't get valid target reading") _LOGGER.error('%s (%s) was busy (temp == -1)', self._name,
return self.device.host)
self._target_temperature = target_temp return
self._current_operation = STATE_HEAT
elif self._tmode == 'Auto': # Map thermostat values into various STATE_ flags.
if self._tstate == 'Cool': self._current_temperature = current_temp
target_temp = self.device.t_cool['raw'] self._fmode = CODE_TO_FAN_MODE[data['fmode']]
if target_temp == -1: self._fstate = CODE_TO_FAN_STATE[data['fstate']]
_LOGGER.error("Couldn't get valid target reading") self._tmode = CODE_TO_TEMP_MODE[data['tmode']]
return self._tstate = CODE_TO_TEMP_STATE[data['tstate']]
self._target_temperature = target_temp
elif self._tstate == 'Heat': self._current_operation = self._tmode
target_temp = self.device.t_heat['raw'] if self._tmode == STATE_COOL:
if target_temp == -1: self._target_temperature = data['t_cool']
_LOGGER.error("Couldn't get valid target reading") elif self._tmode == STATE_HEAT:
return self._target_temperature = data['t_heat']
self._target_temperature = target_temp elif self._tmode == STATE_AUTO:
self._current_operation = STATE_AUTO # This doesn't really work - tstate is only set if the HVAC is
# active. If it's idle, we don't know what to do with the target
# temperature.
if self._tstate == STATE_COOL:
self._target_temperature = data['t_cool']
elif self._tstate == STATE_HEAT:
self._target_temperature = data['t_heat']
else: else:
self._current_operation = STATE_IDLE self._current_operation = STATE_IDLE
@ -191,23 +274,32 @@ class RadioThermostat(ClimateDevice):
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None: if temperature is None:
return return
if self._current_operation == STATE_COOL:
self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_HEAT:
self.device.t_heat = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_AUTO:
if self._tstate == 'Cool':
self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._tstate == 'Heat':
self.device.t_heat = round(temperature * 2.0) / 2.0
if self._hold_temp or self._away: temperature = round_temp(temperature)
self.device.hold = 1
else: if self._current_operation == STATE_COOL:
self.device.hold = 0 self.device.t_cool = temperature
elif self._current_operation == STATE_HEAT:
self.device.t_heat = temperature
elif self._current_operation == STATE_AUTO:
if self._tstate == STATE_COOL:
self.device.t_cool = temperature
elif self._tstate == STATE_HEAT:
self.device.t_heat = temperature
# Only change the hold if requested or if hold mode was turned
# on and we haven't set it yet.
if kwargs.get('hold_changed', False) or not self._hold_set:
if self._hold_temp or self._away:
self.device.hold = 1
self._hold_set = True
else:
self.device.hold = 0
def set_time(self): def set_time(self):
"""Set device time.""" """Set device time."""
# Calling this clears any local temperature override and
# reverts to the scheduled temperature.
now = datetime.datetime.now() now = datetime.datetime.now()
self.device.time = { self.device.time = {
'day': now.weekday(), 'day': now.weekday(),
@ -217,14 +309,14 @@ class RadioThermostat(ClimateDevice):
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set operation mode (auto, cool, heat, off).""" """Set operation mode (auto, cool, heat, off)."""
if operation_mode == STATE_OFF: if operation_mode == STATE_OFF or operation_mode == STATE_AUTO:
self.device.tmode = 0 self.device.tmode = TEMP_MODE_TO_CODE[operation_mode]
elif operation_mode == STATE_AUTO:
self.device.tmode = 3 # Setting t_cool or t_heat automatically changes tmode.
elif operation_mode == STATE_COOL: elif operation_mode == STATE_COOL:
self.device.t_cool = round(self._target_temperature * 2.0) / 2.0 self.device.t_cool = self._target_temperature
elif operation_mode == STATE_HEAT: elif operation_mode == STATE_HEAT:
self.device.t_heat = round(self._target_temperature * 2.0) / 2.0 self.device.t_heat = self._target_temperature
def turn_away_mode_on(self): def turn_away_mode_on(self):
"""Turn away on. """Turn away on.
@ -238,10 +330,11 @@ class RadioThermostat(ClimateDevice):
away_temp = self._away_temps[0] away_temp = self._away_temps[0]
elif self._current_operation == STATE_COOL: elif self._current_operation == STATE_COOL:
away_temp = self._away_temps[1] away_temp = self._away_temps[1]
self._away = True self._away = True
self.set_temperature(temperature=away_temp) self.set_temperature(temperature=away_temp, hold_changed=True)
def turn_away_mode_off(self): def turn_away_mode_off(self):
"""Turn away off.""" """Turn away off."""
self._away = False self._away = False
self.set_temperature(temperature=self._prev_temp) self.set_temperature(temperature=self._prev_temp, hold_changed=True)

View File

@ -15,7 +15,10 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT) ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA) ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_SWING_MODE,
SUPPORT_AUX_HEAT)
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -35,9 +38,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
_FETCH_FIELDS = ','.join([ _FETCH_FIELDS = ','.join([
'room{name}', 'measurements', 'remoteCapabilities', 'room{name}', 'measurements', 'remoteCapabilities',
'acState', 'connectionStatus{isAlive}']) 'acState', 'connectionStatus{isAlive}', 'temperatureUnit'])
_INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS _INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE | SUPPORT_SWING_MODE |
SUPPORT_AUX_HEAT)
@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):
@ -55,7 +62,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
devices.append(SensiboClimate(client, dev)) devices.append(SensiboClimate(client, dev))
except (aiohttp.client_exceptions.ClientConnectorError, except (aiohttp.client_exceptions.ClientConnectorError,
asyncio.TimeoutError): asyncio.TimeoutError):
_LOGGER.exception('Failed to connct to Sensibo servers.') _LOGGER.exception('Failed to connect to Sensibo servers.')
raise PlatformNotReady raise PlatformNotReady
if devices: if devices:
@ -63,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class SensiboClimate(ClimateDevice): class SensiboClimate(ClimateDevice):
"""Representation os a Sensibo device.""" """Representation of a Sensibo device."""
def __init__(self, client, data): def __init__(self, client, data):
"""Build SensiboClimate. """Build SensiboClimate.
@ -75,6 +82,11 @@ class SensiboClimate(ClimateDevice):
self._id = data['id'] self._id = data['id']
self._do_update(data) self._do_update(data)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
def _do_update(self, data): def _do_update(self, data):
self._name = data['room']['name'] self._name = data['room']['name']
self._measurements = data['measurements'] self._measurements = data['measurements']
@ -84,11 +96,16 @@ class SensiboClimate(ClimateDevice):
self._operations = sorted(capabilities['modes'].keys()) self._operations = sorted(capabilities['modes'].keys())
self._current_capabilities = capabilities[ self._current_capabilities = capabilities[
'modes'][self.current_operation] 'modes'][self.current_operation]
temperature_unit_key = self._ac_states['temperatureUnit'] temperature_unit_key = data.get('temperatureUnit') or \
self._temperature_unit = \ self._ac_states.get('temperatureUnit')
TEMP_CELSIUS if temperature_unit_key == 'C' else TEMP_FAHRENHEIT if temperature_unit_key:
self._temperatures_list = self._current_capabilities[ self._temperature_unit = TEMP_CELSIUS if \
'temperatures'][temperature_unit_key]['values'] temperature_unit_key == 'C' else TEMP_FAHRENHEIT
self._temperatures_list = self._current_capabilities[
'temperatures'].get(temperature_unit_key, {}).get('values', [])
else:
self._temperature_unit = self.unit_of_measurement
self._temperatures_list = []
@property @property
def device_state_attributes(self): def device_state_attributes(self):
@ -108,7 +125,7 @@ class SensiboClimate(ClimateDevice):
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._ac_states['targetTemperature'] return self._ac_states.get('targetTemperature')
@property @property
def target_temperature_step(self): def target_temperature_step(self):
@ -133,10 +150,8 @@ class SensiboClimate(ClimateDevice):
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
# This field is not affected by temperature_unit. # This field is not affected by temperatureUnit.
# It is always in C / nativeTemperatureUnit # It is always in C
if 'nativeTemperatureUnit' not in self._ac_states:
return self._measurements['temperature']
return convert_temperature( return convert_temperature(
self._measurements['temperature'], self._measurements['temperature'],
TEMP_CELSIUS, TEMP_CELSIUS,
@ -180,12 +195,14 @@ class SensiboClimate(ClimateDevice):
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return self._temperatures_list[0] return self._temperatures_list[0] \
if len(self._temperatures_list) else super.min_temp()
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return self._temperatures_list[-1] return self._temperatures_list[-1] \
if len(self._temperatures_list) else super.max_temp()
@asyncio.coroutine @asyncio.coroutine
def async_set_temperature(self, **kwargs): def async_set_temperature(self, **kwargs):

View File

@ -7,7 +7,8 @@ https://home-assistant.io/components/climate.tado/
import logging import logging
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import TEMP_CELSIUS
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import (
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.components.tado import DATA_TADO from homeassistant.components.tado import DATA_TADO
@ -43,6 +44,8 @@ OPERATION_LIST = {
CONST_MODE_OFF: 'Off', CONST_MODE_OFF: 'Off',
} }
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tado climate platform.""" """Set up the Tado climate platform."""
@ -127,6 +130,11 @@ class TadoClimate(ClimateDevice):
self._current_operation = CONST_MODE_SMART_SCHEDULE self._current_operation = CONST_MODE_SMART_SCHEDULE
self._overlay_mode = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""

View File

@ -7,7 +7,9 @@ https://home-assistant.io/components/climate.tesla/
import logging import logging
from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT from homeassistant.components.climate import (
ClimateDevice, ENTITY_ID_FORMAT, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE)
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice
from homeassistant.const import ( from homeassistant.const import (
TEMP_FAHRENHEIT, TEMP_CELSIUS, ATTR_TEMPERATURE) TEMP_FAHRENHEIT, TEMP_CELSIUS, ATTR_TEMPERATURE)
@ -18,6 +20,8 @@ DEPENDENCIES = ['tesla']
OPERATION_LIST = [STATE_ON, STATE_OFF] OPERATION_LIST = [STATE_ON, STATE_OFF]
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tesla climate platform.""" """Set up the Tesla climate platform."""
@ -36,6 +40,11 @@ class TeslaThermostat(TeslaDevice, ClimateDevice):
self._target_temperature = None self._target_temperature = None
self._temperature = None self._temperature = None
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation ie. On or Off.""" """Return current operation ie. On or Off."""

View File

@ -10,9 +10,11 @@ https://home-assistant.io/components/climate.toon/
import homeassistant.components.toon as toon_main import homeassistant.components.toon as toon_main
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, ATTR_TEMPERATURE, STATE_PERFORMANCE, STATE_HEAT, STATE_ECO, ClimateDevice, ATTR_TEMPERATURE, STATE_PERFORMANCE, STATE_HEAT, STATE_ECO,
STATE_COOL) STATE_COOL, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import TEMP_CELSIUS
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Toon thermostat.""" """Set up the Toon thermostat."""
@ -38,6 +40,11 @@ class ThermostatDevice(ClimateDevice):
STATE_COOL, STATE_COOL,
] ]
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def name(self): def name(self):
"""Name of this Thermostat.""" """Name of this Thermostat."""

View File

@ -7,7 +7,9 @@ https://home-assistant.io/components/switch.vera/
import logging import logging
from homeassistant.util import convert from homeassistant.util import convert
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT from homeassistant.components.climate import (
ClimateDevice, ENTITY_ID_FORMAT, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE)
from homeassistant.const import ( from homeassistant.const import (
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
TEMP_CELSIUS, TEMP_CELSIUS,
@ -23,6 +25,9 @@ _LOGGER = logging.getLogger(__name__)
OPERATION_LIST = ['Heat', 'Cool', 'Auto Changeover', 'Off'] OPERATION_LIST = ['Heat', 'Cool', 'Auto Changeover', 'Off']
FAN_OPERATION_LIST = ['On', 'Auto', 'Cycle'] FAN_OPERATION_LIST = ['On', 'Auto', 'Cycle']
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_FAN_MODE)
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up of Vera thermostats.""" """Set up of Vera thermostats."""
@ -39,6 +44,11 @@ class VeraThermostat(VeraDevice, ClimateDevice):
VeraDevice.__init__(self, vera_device, controller) VeraDevice.__init__(self, vera_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id)
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation ie. heat, cool, idle.""" """Return current operation ie. heat, cool, idle."""

View File

@ -11,7 +11,10 @@ from homeassistant.components.climate import (
STATE_ECO, STATE_GAS, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ELECTRIC, STATE_ECO, STATE_GAS, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ELECTRIC,
STATE_FAN_ONLY, STATE_HEAT_PUMP, ATTR_TEMPERATURE, STATE_HIGH_DEMAND, STATE_FAN_ONLY, STATE_HEAT_PUMP, ATTR_TEMPERATURE, STATE_HIGH_DEMAND,
STATE_PERFORMANCE, ATTR_TARGET_TEMP_LOW, ATTR_CURRENT_HUMIDITY, STATE_PERFORMANCE, ATTR_TARGET_TEMP_LOW, ATTR_CURRENT_HUMIDITY,
ATTR_TARGET_TEMP_HIGH, ClimateDevice) ATTR_TARGET_TEMP_HIGH, ClimateDevice, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW,
SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE, SUPPORT_FAN_MODE,
SUPPORT_AUX_HEAT)
from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.components.wink import DOMAIN, WinkDevice
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, TEMP_CELSIUS, STATE_UNKNOWN, PRECISION_TENTHS) STATE_ON, STATE_OFF, TEMP_CELSIUS, STATE_UNKNOWN, PRECISION_TENTHS)
@ -50,6 +53,17 @@ HA_STATE_TO_WINK = {
WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()} WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()}
SUPPORT_FLAGS_THERMOSTAT = (
SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH |
SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE | SUPPORT_FAN_MODE | SUPPORT_AUX_HEAT)
SUPPORT_FLAGS_AC = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_FAN_MODE)
SUPPORT_FLAGS_HEATER = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
SUPPORT_AWAY_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Wink climate devices.""" """Set up the Wink climate devices."""
@ -72,6 +86,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class WinkThermostat(WinkDevice, ClimateDevice): class WinkThermostat(WinkDevice, ClimateDevice):
"""Representation of a Wink thermostat.""" """Representation of a Wink thermostat."""
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS_THERMOSTAT
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Callback when entity is added to hass.""" """Callback when entity is added to hass."""
@ -353,6 +372,11 @@ class WinkThermostat(WinkDevice, ClimateDevice):
class WinkAC(WinkDevice, ClimateDevice): class WinkAC(WinkDevice, ClimateDevice):
"""Representation of a Wink air conditioner.""" """Representation of a Wink air conditioner."""
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS_AC
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
@ -471,6 +495,11 @@ class WinkAC(WinkDevice, ClimateDevice):
class WinkWaterHeater(WinkDevice, ClimateDevice): class WinkWaterHeater(WinkDevice, ClimateDevice):
"""Representation of a Wink water heater.""" """Representation of a Wink water heater."""
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS_HEATER
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""

View File

@ -7,8 +7,9 @@ https://home-assistant.io/components/climate.zwave/
# Because we do not compile openzwave on CI # Because we do not compile openzwave on CI
# pylint: disable=import-error # pylint: disable=import-error
import logging import logging
from homeassistant.components.climate import DOMAIN from homeassistant.components.climate import (
from homeassistant.components.climate import ClimateDevice DOMAIN, ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE)
from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
from homeassistant.const import ( from homeassistant.const import (
@ -70,6 +71,18 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
self._zxt_120 = 1 self._zxt_120 = 1
self.update_properties() self.update_properties()
@property
def supported_features(self):
"""Return the list of supported features."""
support = SUPPORT_TARGET_TEMPERATURE
if self.values.fan_mode:
support |= SUPPORT_FAN_MODE
if self.values.mode:
support |= SUPPORT_OPERATION_MODE
if self._zxt_120 == 1 and self.values.zxt_120_swing_mode:
support |= SUPPORT_SWING_MODE
return support
def update_properties(self): def update_properties(self):
"""Handle the data changes for node values.""" """Handle the data changes for node values."""
# Operation Mode # Operation Mode

View File

@ -104,6 +104,11 @@ class Cloud:
self.region = info['region'] self.region = info['region']
self.relayer = info['relayer'] self.relayer = info['relayer']
@property
def cognito_email_based(self):
"""Return if cognito is email based."""
return not self.user_pool_id.endswith('GmV')
@property @property
def is_logged_in(self): def is_logged_in(self):
"""Get if cloud is logged in.""" """Get if cloud is logged in."""

View File

@ -69,7 +69,10 @@ def register(cloud, email, password):
cognito = _cognito(cloud) cognito = _cognito(cloud)
try: try:
cognito.register(_generate_username(email), password, email=email) if cloud.cognito_email_based:
cognito.register(email, password, email=email)
else:
cognito.register(_generate_username(email), password, email=email)
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
@ -80,7 +83,11 @@ def confirm_register(cloud, confirmation_code, email):
cognito = _cognito(cloud) cognito = _cognito(cloud)
try: try:
cognito.confirm_sign_up(confirmation_code, _generate_username(email)) if cloud.cognito_email_based:
cognito.confirm_sign_up(confirmation_code, email)
else:
cognito.confirm_sign_up(confirmation_code,
_generate_username(email))
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
@ -89,7 +96,11 @@ def forgot_password(cloud, email):
"""Initiate forgotten password flow.""" """Initiate forgotten password flow."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
cognito = _cognito(cloud, username=_generate_username(email)) if cloud.cognito_email_based:
cognito = _cognito(cloud, username=email)
else:
cognito = _cognito(cloud, username=_generate_username(email))
try: try:
cognito.initiate_forgot_password() cognito.initiate_forgot_password()
except ClientError as err: except ClientError as err:
@ -100,7 +111,11 @@ def confirm_forgot_password(cloud, confirmation_code, email, new_password):
"""Confirm forgotten password code and change password.""" """Confirm forgotten password code and change password."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
cognito = _cognito(cloud, username=_generate_username(email)) if cloud.cognito_email_based:
cognito = _cognito(cloud, username=email)
else:
cognito = _cognito(cloud, username=_generate_username(email))
try: try:
cognito.confirm_forgot_password(confirmation_code, new_password) cognito.confirm_forgot_password(confirmation_code, new_password)
except ClientError as err: except ClientError as err:

View File

@ -65,12 +65,12 @@ class CloudLoginView(HomeAssistantView):
url = '/api/cloud/login' url = '/api/cloud/login'
name = 'api:cloud:login' name = 'api:cloud:login'
@asyncio.coroutine
@_handle_cloud_errors @_handle_cloud_errors
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('email'): str, vol.Required('email'): str,
vol.Required('password'): str, vol.Required('password'): str,
})) }))
@asyncio.coroutine
def post(self, request, data): def post(self, request, data):
"""Handle login request.""" """Handle login request."""
hass = request.app['hass'] hass = request.app['hass']
@ -92,8 +92,8 @@ class CloudLogoutView(HomeAssistantView):
url = '/api/cloud/logout' url = '/api/cloud/logout'
name = 'api:cloud:logout' name = 'api:cloud:logout'
@asyncio.coroutine
@_handle_cloud_errors @_handle_cloud_errors
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Handle logout request.""" """Handle logout request."""
hass = request.app['hass'] hass = request.app['hass']
@ -129,12 +129,12 @@ class CloudRegisterView(HomeAssistantView):
url = '/api/cloud/register' url = '/api/cloud/register'
name = 'api:cloud:register' name = 'api:cloud:register'
@asyncio.coroutine
@_handle_cloud_errors @_handle_cloud_errors
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('email'): str, vol.Required('email'): str,
vol.Required('password'): vol.All(str, vol.Length(min=6)), vol.Required('password'): vol.All(str, vol.Length(min=6)),
})) }))
@asyncio.coroutine
def post(self, request, data): def post(self, request, data):
"""Handle registration request.""" """Handle registration request."""
hass = request.app['hass'] hass = request.app['hass']
@ -153,12 +153,12 @@ class CloudConfirmRegisterView(HomeAssistantView):
url = '/api/cloud/confirm_register' url = '/api/cloud/confirm_register'
name = 'api:cloud:confirm_register' name = 'api:cloud:confirm_register'
@asyncio.coroutine
@_handle_cloud_errors @_handle_cloud_errors
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('confirmation_code'): str, vol.Required('confirmation_code'): str,
vol.Required('email'): str, vol.Required('email'): str,
})) }))
@asyncio.coroutine
def post(self, request, data): def post(self, request, data):
"""Handle registration confirmation request.""" """Handle registration confirmation request."""
hass = request.app['hass'] hass = request.app['hass']
@ -178,11 +178,11 @@ class CloudForgotPasswordView(HomeAssistantView):
url = '/api/cloud/forgot_password' url = '/api/cloud/forgot_password'
name = 'api:cloud:forgot_password' name = 'api:cloud:forgot_password'
@asyncio.coroutine
@_handle_cloud_errors @_handle_cloud_errors
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('email'): str, vol.Required('email'): str,
})) }))
@asyncio.coroutine
def post(self, request, data): def post(self, request, data):
"""Handle forgot password request.""" """Handle forgot password request."""
hass = request.app['hass'] hass = request.app['hass']
@ -201,13 +201,13 @@ class CloudConfirmForgotPasswordView(HomeAssistantView):
url = '/api/cloud/confirm_forgot_password' url = '/api/cloud/confirm_forgot_password'
name = 'api:cloud:confirm_forgot_password' name = 'api:cloud:confirm_forgot_password'
@asyncio.coroutine
@_handle_cloud_errors @_handle_cloud_errors
@RequestDataValidator(vol.Schema({ @RequestDataValidator(vol.Schema({
vol.Required('confirmation_code'): str, vol.Required('confirmation_code'): str,
vol.Required('email'): str, vol.Required('email'): str,
vol.Required('new_password'): vol.All(str, vol.Length(min=6)) vol.Required('new_password'): vol.All(str, vol.Length(min=6))
})) }))
@asyncio.coroutine
def post(self, request, data): def post(self, request, data):
"""Handle forgot password confirm request.""" """Handle forgot password confirm request."""
hass = request.app['hass'] hass = request.app['hass']

View File

@ -59,13 +59,6 @@ class CloudIoT:
if self.state == STATE_CONNECTED: if self.state == STATE_CONNECTED:
raise RuntimeError('Already connected') raise RuntimeError('Already connected')
self.state = STATE_CONNECTING
self.close_requested = False
remove_hass_stop_listener = None
session = async_get_clientsession(self.cloud.hass)
client = None
disconnect_warn = None
@asyncio.coroutine @asyncio.coroutine
def _handle_hass_stop(event): def _handle_hass_stop(event):
"""Handle Home Assistant shutting down.""" """Handle Home Assistant shutting down."""
@ -73,6 +66,14 @@ class CloudIoT:
remove_hass_stop_listener = None remove_hass_stop_listener = None
yield from self.disconnect() yield from self.disconnect()
self.state = STATE_CONNECTING
self.close_requested = False
remove_hass_stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
session = async_get_clientsession(self.cloud.hass)
client = None
disconnect_warn = None
try: try:
yield from hass.async_add_job(auth_api.check_token, self.cloud) yield from hass.async_add_job(auth_api.check_token, self.cloud)
@ -83,9 +84,6 @@ class CloudIoT:
}) })
self.tries = 0 self.tries = 0
remove_hass_stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
_LOGGER.info('Connected') _LOGGER.info('Connected')
self.state = STATE_CONNECTED self.state = STATE_CONNECTED

View File

@ -1,8 +1,8 @@
"""Provide configuration end points for Groups.""" """Provide configuration end points for Groups."""
import asyncio import asyncio
from homeassistant.const import SERVICE_RELOAD
from homeassistant.components.config import EditKeyBasedConfigView from homeassistant.components.config import EditKeyBasedConfigView
from homeassistant.components.group import GROUP_SCHEMA from homeassistant.components.group import DOMAIN, GROUP_SCHEMA
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -12,7 +12,13 @@ CONFIG_PATH = 'groups.yaml'
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass): def async_setup(hass):
"""Set up the Group config API.""" """Set up the Group config API."""
@asyncio.coroutine
def hook(hass):
"""post_write_hook for Config View that reloads groups."""
yield from hass.services.async_call(DOMAIN, SERVICE_RELOAD)
hass.http.register_view(EditKeyBasedConfigView( hass.http.register_view(EditKeyBasedConfigView(
'group', 'config', CONFIG_PATH, cv.slug, GROUP_SCHEMA 'group', 'config', CONFIG_PATH, cv.slug, GROUP_SCHEMA,
post_write_hook=hook
)) ))
return True return True

View File

@ -50,15 +50,19 @@ def async_request_config(
Will return an ID to be used for sequent calls. Will return an ID to be used for sequent calls.
""" """
if link_name is not None and link_url is not None:
description += '\n\n[{}]({})'.format(link_name, link_url)
if description_image is not None:
description += '\n\n![Description image]({})'.format(description_image)
instance = hass.data.get(_KEY_INSTANCE) instance = hass.data.get(_KEY_INSTANCE)
if instance is None: if instance is None:
instance = hass.data[_KEY_INSTANCE] = Configurator(hass) instance = hass.data[_KEY_INSTANCE] = Configurator(hass)
request_id = instance.async_request_config( request_id = instance.async_request_config(
name, callback, name, callback, description, submit_caption, fields, entity_picture)
description, description_image, submit_caption,
fields, link_name, link_url, entity_picture)
if DATA_REQUESTS not in hass.data: if DATA_REQUESTS not in hass.data:
hass.data[DATA_REQUESTS] = {} hass.data[DATA_REQUESTS] = {}
@ -137,9 +141,8 @@ class Configurator(object):
@async_callback @async_callback
def async_request_config( def async_request_config(
self, name, callback, self, name, callback, description, submit_caption, fields,
description, description_image, submit_caption, entity_picture):
fields, link_name, link_url, entity_picture):
"""Set up a request for configuration.""" """Set up a request for configuration."""
entity_id = async_generate_entity_id( entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, name, hass=self.hass) ENTITY_ID_FORMAT, name, hass=self.hass)
@ -161,10 +164,7 @@ class Configurator(object):
data.update({ data.update({
key: value for key, value in [ key: value for key, value in [
(ATTR_DESCRIPTION, description), (ATTR_DESCRIPTION, description),
(ATTR_DESCRIPTION_IMAGE, description_image),
(ATTR_SUBMIT_CAPTION, submit_caption), (ATTR_SUBMIT_CAPTION, submit_caption),
(ATTR_LINK_NAME, link_name),
(ATTR_LINK_URL, link_url),
] if value is not None ] if value is not None
}) })

View File

@ -14,7 +14,7 @@ import voluptuous as vol
from homeassistant import core from homeassistant import core
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, HTTP_BAD_REQUEST) ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
from homeassistant.helpers import intent, config_validation as cv from homeassistant.helpers import intent, config_validation as cv
from homeassistant.components import http from homeassistant.components import http
@ -39,6 +39,10 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
}) })
})}, extra=vol.ALLOW_EXTRA) })}, extra=vol.ALLOW_EXTRA)
INTENT_TURN_ON = 'HassTurnOn'
INTENT_TURN_OFF = 'HassTurnOff'
REGEX_TYPE = type(re.compile(''))
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -60,7 +64,11 @@ def async_register(hass, intent_type, utterances):
if conf is None: if conf is None:
conf = intents[intent_type] = [] conf = intents[intent_type] = []
conf.extend(_create_matcher(utterance) for utterance in utterances) for utterance in utterances:
if isinstance(utterance, REGEX_TYPE):
conf.append(utterance)
else:
conf.append(_create_matcher(utterance))
@asyncio.coroutine @asyncio.coroutine
@ -93,6 +101,13 @@ def async_setup(hass, config):
hass.http.register_view(ConversationProcessView) hass.http.register_view(ConversationProcessView)
hass.helpers.intent.async_register(TurnOnIntent())
hass.helpers.intent.async_register(TurnOffIntent())
async_register(hass, INTENT_TURN_ON,
['Turn {name} on', 'Turn on {name}'])
async_register(hass, INTENT_TURN_OFF, [
'Turn {name} off', 'Turn off {name}'])
return True return True
@ -128,48 +143,84 @@ def _process(hass, text):
if not match: if not match:
continue continue
response = yield from intent.async_handle( response = yield from hass.helpers.intent.async_handle(
hass, DOMAIN, intent_type, DOMAIN, intent_type,
{key: {'value': value} for key, value {key: {'value': value} for key, value
in match.groupdict().items()}, text) in match.groupdict().items()}, text)
return response return response
@core.callback
def _match_entity(hass, name):
"""Match a name to an entity."""
from fuzzywuzzy import process as fuzzyExtract from fuzzywuzzy import process as fuzzyExtract
text = text.lower()
match = REGEX_TURN_COMMAND.match(text)
if not match:
_LOGGER.error("Unable to process: %s", text)
return None
name, command = match.groups()
entities = {state.entity_id: state.name for state entities = {state.entity_id: state.name for state
in hass.states.async_all()} in hass.states.async_all()}
entity_ids = fuzzyExtract.extractOne( entity_id = fuzzyExtract.extractOne(
name, entities, score_cutoff=65)[2] name, entities, score_cutoff=65)[2]
return hass.states.get(entity_id) if entity_id else None
if not entity_ids:
_LOGGER.error(
"Could not find entity id %s from text %s", name, text)
return None
if command == 'on': class TurnOnIntent(intent.IntentHandler):
"""Handle turning item on intents."""
intent_type = INTENT_TURN_ON
slot_schema = {
'name': cv.string,
}
@asyncio.coroutine
def async_handle(self, intent_obj):
"""Handle turn on intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
name = slots['name']['value']
entity = _match_entity(hass, name)
if not entity:
_LOGGER.error("Could not find entity id for %s", name)
return None
yield from hass.services.async_call( yield from hass.services.async_call(
core.DOMAIN, SERVICE_TURN_ON, { core.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity_ids, ATTR_ENTITY_ID: entity.entity_id,
}, blocking=True) }, blocking=True)
elif command == 'off': response = intent_obj.create_response()
response.async_set_speech(
'Turned on {}'.format(entity.name))
return response
class TurnOffIntent(intent.IntentHandler):
"""Handle turning item off intents."""
intent_type = INTENT_TURN_OFF
slot_schema = {
'name': cv.string,
}
@asyncio.coroutine
def async_handle(self, intent_obj):
"""Handle turn off intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
name = slots['name']['value']
entity = _match_entity(hass, name)
if not entity:
_LOGGER.error("Could not find entity id for %s", name)
return None
yield from hass.services.async_call( yield from hass.services.async_call(
core.DOMAIN, SERVICE_TURN_OFF, { core.DOMAIN, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity_ids, ATTR_ENTITY_ID: entity.entity_id,
}, blocking=True) }, blocking=True)
else: response = intent_obj.create_response()
_LOGGER.error('Got unsupported command %s from text %s', response.async_set_speech(
command, text) 'Turned off {}'.format(entity.name))
return response
return None
class ConversationProcessView(http.HomeAssistantView): class ConversationProcessView(http.HomeAssistantView):
@ -178,23 +229,15 @@ class ConversationProcessView(http.HomeAssistantView):
url = '/api/conversation/process' url = '/api/conversation/process'
name = "api:conversation:process" name = "api:conversation:process"
@http.RequestDataValidator(vol.Schema({
vol.Required('text'): str,
}))
@asyncio.coroutine @asyncio.coroutine
def post(self, request): def post(self, request, data):
"""Send a request for processing.""" """Send a request for processing."""
hass = request.app['hass'] hass = request.app['hass']
try:
data = yield from request.json()
except ValueError:
return self.json_message('Invalid JSON specified',
HTTP_BAD_REQUEST)
text = data.get('text') intent_result = yield from _process(hass, data['text'])
if text is None:
return self.json_message('Missing "text" key in JSON.',
HTTP_BAD_REQUEST)
intent_result = yield from _process(hass, text)
if intent_result is None: if intent_result is None:
intent_result = intent.IntentResponse() intent_result = intent.IntentResponse()

View File

@ -0,0 +1,73 @@
"""
Support for Tahoma cover - shutters etc.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.tahoma/
"""
import logging
from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT
from homeassistant.components.tahoma import (
DOMAIN as TAHOMA_DOMAIN, TahomaDevice)
DEPENDENCIES = ['tahoma']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Tahoma covers."""
controller = hass.data[TAHOMA_DOMAIN]['controller']
devices = []
for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']:
devices.append(TahomaCover(device, controller))
add_devices(devices, True)
class TahomaCover(TahomaDevice, CoverDevice):
"""Representation a Tahoma Cover."""
def __init__(self, tahoma_device, controller):
"""Initialize the Tahoma device."""
super().__init__(tahoma_device, controller)
self.entity_id = ENTITY_ID_FORMAT.format(self.unique_id)
def update(self):
"""Update method."""
self.controller.get_states([self.tahoma_device])
@property
def current_cover_position(self):
"""
Return current position of cover.
0 is closed, 100 is fully open.
"""
position = 100 - self.tahoma_device.active_states['core:ClosureState']
if position <= 5:
return 0
if position >= 95:
return 100
return position
def set_cover_position(self, position, **kwargs):
"""Move the cover to a specific position."""
self.apply_action('setPosition', 100 - position)
@property
def is_closed(self):
"""Return if the cover is closed."""
if self.current_cover_position is not None:
return self.current_cover_position == 0
def open_cover(self, **kwargs):
"""Open the cover."""
self.apply_action('open')
def close_cover(self, **kwargs):
"""Close the cover."""
self.apply_action('close')
def stop_cover(self, **kwargs):
"""Stop the cover."""
self.apply_action('stopIdentify')

View File

@ -0,0 +1,134 @@
"""
Support for Unifi AP direct access.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.unifi_direct/
"""
import logging
import json
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_PASSWORD, CONF_USERNAME,
CONF_PORT)
REQUIREMENTS = ['pexpect==4.0.1']
_LOGGER = logging.getLogger(__name__)
DEFAULT_SSH_PORT = 22
UNIFI_COMMAND = 'mca-dump | tr -d "\n"'
UNIFI_SSID_TABLE = "vap_table"
UNIFI_CLIENT_TABLE = "sta_table"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port
})
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a Unifi direct scanner."""
scanner = UnifiDeviceScanner(config[DOMAIN])
if not scanner.connected:
return False
return scanner
class UnifiDeviceScanner(DeviceScanner):
"""This class queries Unifi wireless access point."""
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.port = config[CONF_PORT]
self.ssh = None
self.connected = False
self.last_results = {}
self._connect()
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
result = _response_to_json(self._get_update())
if result:
self.last_results = result
return self.last_results.keys()
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
hostname = next((
value.get('hostname') for key, value in self.last_results.items()
if key.upper() == device.upper()), None)
if hostname is not None:
hostname = str(hostname)
return hostname
def _connect(self):
"""Connect to the Unifi AP SSH server."""
from pexpect import pxssh, exceptions
self.ssh = pxssh.pxssh()
try:
self.ssh.login(self.host, self.username,
password=self.password, port=self.port)
self.connected = True
except exceptions.EOF:
_LOGGER.error("Connection refused. SSH enabled?")
self._disconnect()
def _disconnect(self):
"""Disconnect the current SSH connection."""
# pylint: disable=broad-except
try:
self.ssh.logout()
except Exception:
pass
finally:
self.ssh = None
self.connected = False
def _get_update(self):
from pexpect import pxssh
try:
if not self.connected:
self._connect()
self.ssh.sendline(UNIFI_COMMAND)
self.ssh.prompt()
return self.ssh.before
except pxssh.ExceptionPxssh as err:
_LOGGER.error("Unexpected SSH error: %s", str(err))
self._disconnect()
return None
except AssertionError as err:
_LOGGER.error("Connection to AP unavailable: %s", str(err))
self._disconnect()
return None
def _response_to_json(response):
try:
json_response = json.loads(str(response)[31:-1].replace("\\", ""))
_LOGGER.debug(str(json_response))
ssid_table = json_response.get(UNIFI_SSID_TABLE)
active_clients = {}
for ssid in ssid_table:
client_table = ssid.get(UNIFI_CLIENT_TABLE)
for client in client_table:
active_clients[client.get("mac")] = client
return active_clients
except ValueError:
_LOGGER.error("Failed to decode response from AP.")
return {}

View File

@ -35,6 +35,7 @@ SERVICE_AXIS = 'axis'
SERVICE_APPLE_TV = 'apple_tv' SERVICE_APPLE_TV = 'apple_tv'
SERVICE_WINK = 'wink' SERVICE_WINK = 'wink'
SERVICE_XIAOMI_GW = 'xiaomi_gw' SERVICE_XIAOMI_GW = 'xiaomi_gw'
SERVICE_TELLDUSLIVE = 'tellstick'
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_HASS_IOS_APP: ('ios', None),
@ -46,6 +47,7 @@ SERVICE_HANDLERS = {
SERVICE_APPLE_TV: ('apple_tv', None), SERVICE_APPLE_TV: ('apple_tv', None),
SERVICE_WINK: ('wink', None), SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
'philips_hue': ('light', 'hue'), 'philips_hue': ('light', 'hue'),
'google_cast': ('media_player', 'cast'), 'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'), 'panasonic_viera': ('media_player', 'panasonic_viera'),

View File

@ -0,0 +1,240 @@
"""
Support for Dominos Pizza ordering.
The Dominos Pizza component ceates a service which can be invoked to order
from their menu
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/dominos/.
"""
import logging
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components import http
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
# The domain of your component. Should be equal to the name of your component.
DOMAIN = 'dominos'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
ATTR_COUNTRY = 'country_code'
ATTR_FIRST_NAME = 'first_name'
ATTR_LAST_NAME = 'last_name'
ATTR_EMAIL = 'email'
ATTR_PHONE = 'phone'
ATTR_ADDRESS = 'address'
ATTR_ORDERS = 'orders'
ATTR_SHOW_MENU = 'show_menu'
ATTR_ORDER_ENTITY = 'order_entity_id'
ATTR_ORDER_NAME = 'name'
ATTR_ORDER_CODES = 'codes'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
MIN_TIME_BETWEEN_STORE_UPDATES = timedelta(minutes=3330)
REQUIREMENTS = ['pizzapi==0.0.3']
DEPENDENCIES = ['http']
_ORDERS_SCHEMA = vol.Schema({
vol.Required(ATTR_ORDER_NAME): cv.string,
vol.Required(ATTR_ORDER_CODES): vol.All(cv.ensure_list, [cv.string]),
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(ATTR_COUNTRY): cv.string,
vol.Required(ATTR_FIRST_NAME): cv.string,
vol.Required(ATTR_LAST_NAME): cv.string,
vol.Required(ATTR_EMAIL): cv.string,
vol.Required(ATTR_PHONE): cv.string,
vol.Required(ATTR_ADDRESS): cv.string,
vol.Optional(ATTR_SHOW_MENU): cv.boolean,
vol.Optional(ATTR_ORDERS): vol.All(cv.ensure_list, [_ORDERS_SCHEMA]),
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up is called when Home Assistant is loading our component."""
dominos = Dominos(hass, config)
component = EntityComponent(_LOGGER, DOMAIN, hass)
hass.data[DOMAIN] = {}
entities = []
conf = config[DOMAIN]
hass.services.register(DOMAIN, 'order', dominos.handle_order)
if conf.get(ATTR_SHOW_MENU):
hass.http.register_view(DominosProductListView(dominos))
for order_info in conf.get(ATTR_ORDERS):
order = DominosOrder(order_info, dominos)
entities.append(order)
component.add_entities(entities)
# Return boolean to indicate that initialization was successfully.
return True
class Dominos():
"""Main Dominos service."""
def __init__(self, hass, config):
"""Set up main service."""
conf = config[DOMAIN]
from pizzapi import Address, Customer, Store
self.hass = hass
self.customer = Customer(
conf.get(ATTR_FIRST_NAME),
conf.get(ATTR_LAST_NAME),
conf.get(ATTR_EMAIL),
conf.get(ATTR_PHONE),
conf.get(ATTR_ADDRESS))
self.address = Address(
*self.customer.address.split(','),
country=conf.get(ATTR_COUNTRY))
self.country = conf.get(ATTR_COUNTRY)
self.closest_store = Store()
def handle_order(self, call):
"""Handle ordering pizza."""
entity_ids = call.data.get(ATTR_ORDER_ENTITY, None)
target_orders = [order for order in self.hass.data[DOMAIN]['entities']
if order.entity_id in entity_ids]
for order in target_orders:
order.place()
@Throttle(MIN_TIME_BETWEEN_STORE_UPDATES)
def update_closest_store(self):
"""Update the shared closest store (if open)."""
from pizzapi.address import StoreException
try:
self.closest_store = self.address.closest_store()
except StoreException:
self.closest_store = False
def get_menu(self):
"""Return the products from the closest stores menu."""
if self.closest_store is False:
_LOGGER.warning('Cannot get menu. Store may be closed')
return
menu = self.closest_store.get_menu()
product_entries = []
for product in menu.products:
item = {}
if isinstance(product.menu_data['Variants'], list):
variants = ', '.join(product.menu_data['Variants'])
else:
variants = product.menu_data['Variants']
item['name'] = product.name
item['variants'] = variants
product_entries.append(item)
return product_entries
class DominosProductListView(http.HomeAssistantView):
"""View to retrieve product list content."""
url = '/api/dominos'
name = "api:dominos"
def __init__(self, dominos):
"""Initialize suite view."""
self.dominos = dominos
@callback
def get(self, request):
"""Retrieve if API is running."""
return self.json(self.dominos.get_menu())
class DominosOrder(Entity):
"""Represents a Dominos order entity."""
def __init__(self, order_info, dominos):
"""Set up the entity."""
self._name = order_info['name']
self._product_codes = order_info['codes']
self._orderable = False
self.dominos = dominos
@property
def name(self):
"""Return the orders name."""
return self._name
@property
def product_codes(self):
"""Return the orders product codes."""
return self._product_codes
@property
def orderable(self):
"""Return the true if orderable."""
return self._orderable
@property
def state(self):
"""Return the state either closed, orderable or unorderable."""
if self.dominos.closest_store is False:
return 'closed'
else:
return 'orderable' if self._orderable else 'unorderable'
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update the order state and refreshes the store."""
from pizzapi.address import StoreException
try:
self.dominos.update_closest_store()
except StoreException:
self._orderable = False
return
try:
order = self.order()
order.pay_with()
self._orderable = True
except StoreException:
self._orderable = False
def order(self):
"""Create the order object."""
from pizzapi import Order
order = Order(
self.dominos.closest_store,
self.dominos.customer,
self.dominos.address,
self.dominos.country)
for code in self._product_codes:
order.add_item(code)
return order
def place(self):
"""Place the order."""
from pizzapi.address import StoreException
try:
order = self.order()
order.place()
except StoreException:
self._orderable = False
_LOGGER.warning(
'Attempted to order Dominos - Order invalid or store closed')

View File

@ -6,7 +6,7 @@ import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['DoorBirdPy==0.0.4'] REQUIREMENTS = ['DoorBirdPy==0.1.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -14,8 +14,9 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.util.json import save_json
REQUIREMENTS = ['python-ecobee-api==0.0.10'] REQUIREMENTS = ['python-ecobee-api==0.0.12']
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -81,6 +82,7 @@ def setup_ecobee(hass, network, config):
hass, 'climate', DOMAIN, {'hold_temp': hold_temp}, config) hass, 'climate', DOMAIN, {'hold_temp': hold_temp}, config)
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'weather', DOMAIN, {}, config)
class EcobeeData(object): class EcobeeData(object):
@ -110,12 +112,10 @@ def setup(hass, config):
if 'ecobee' in _CONFIGURING: if 'ecobee' in _CONFIGURING:
return return
from pyecobee import config_from_file
# Create ecobee.conf if it doesn't exist # Create ecobee.conf if it doesn't exist
if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)): if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)):
jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)} jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)}
config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) save_json(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig)
NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE))

View File

@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/emulated_hue/ https://home-assistant.io/components/emulated_hue/
""" """
import asyncio import asyncio
import json
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -16,8 +15,10 @@ from homeassistant.const import (
) )
from homeassistant.components.http import REQUIREMENTS # NOQA from homeassistant.components.http import REQUIREMENTS # NOQA
from homeassistant.components.http import HomeAssistantWSGI from homeassistant.components.http import HomeAssistantWSGI
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.deprecation import get_deprecated from homeassistant.helpers.deprecation import get_deprecated
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
from .hue_api import ( from .hue_api import (
HueUsernameView, HueAllLightsStateView, HueOneLightStateView, HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
HueOneLightChangeView) HueOneLightChangeView)
@ -136,7 +137,7 @@ class Config(object):
self.host_ip_addr = conf.get(CONF_HOST_IP) self.host_ip_addr = conf.get(CONF_HOST_IP)
if self.host_ip_addr is None: if self.host_ip_addr is None:
self.host_ip_addr = util.get_local_ip() self.host_ip_addr = util.get_local_ip()
_LOGGER.warning( _LOGGER.info(
"Listen IP address not specified, auto-detected address is %s", "Listen IP address not specified, auto-detected address is %s",
self.host_ip_addr) self.host_ip_addr)
@ -144,7 +145,7 @@ class Config(object):
self.listen_port = conf.get(CONF_LISTEN_PORT) self.listen_port = conf.get(CONF_LISTEN_PORT)
if not isinstance(self.listen_port, int): if not isinstance(self.listen_port, int):
self.listen_port = DEFAULT_LISTEN_PORT self.listen_port = DEFAULT_LISTEN_PORT
_LOGGER.warning( _LOGGER.info(
"Listen port not specified, defaulting to %s", "Listen port not specified, defaulting to %s",
self.listen_port) self.listen_port)
@ -187,7 +188,7 @@ class Config(object):
return entity_id return entity_id
if self.numbers is None: if self.numbers is None:
self.numbers = self._load_numbers_json() self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
# Google Home # Google Home
for number, ent_id in self.numbers.items(): for number, ent_id in self.numbers.items():
@ -198,7 +199,7 @@ class Config(object):
if self.numbers: if self.numbers:
number = str(max(int(k) for k in self.numbers) + 1) number = str(max(int(k) for k in self.numbers) + 1)
self.numbers[number] = entity_id self.numbers[number] = entity_id
self._save_numbers_json() save_json(self.hass.config.path(NUMBERS_FILE), self.numbers)
return number return number
def number_to_entity_id(self, number): def number_to_entity_id(self, number):
@ -207,7 +208,7 @@ class Config(object):
return number return number
if self.numbers is None: if self.numbers is None:
self.numbers = self._load_numbers_json() self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
# Google Home # Google Home
assert isinstance(number, str) assert isinstance(number, str)
@ -244,25 +245,11 @@ class Config(object):
return is_default_exposed or expose return is_default_exposed or expose
def _load_numbers_json(self):
"""Set up helper method to load numbers json."""
try:
with open(self.hass.config.path(NUMBERS_FILE),
encoding='utf-8') as fil:
return json.loads(fil.read())
except (OSError, ValueError) as err:
# OSError if file not found or unaccessible/no permissions
# ValueError if could not parse JSON
if not isinstance(err, FileNotFoundError):
_LOGGER.warning("Failed to open %s: %s", NUMBERS_FILE, err)
return {}
def _save_numbers_json(self): def _load_json(filename):
"""Set up helper method to save numbers json.""" """Wrapper, because we actually want to handle invalid json."""
try: try:
with open(self.hass.config.path(NUMBERS_FILE), 'w', return load_json(filename)
encoding='utf-8') as fil: except HomeAssistantError:
fil.write(json.dumps(self.numbers)) pass
except OSError as err: return {}
# OSError if file write permissions
_LOGGER.warning("Failed to write %s: %s", NUMBERS_FILE, err)

View File

@ -4,9 +4,7 @@ Support for Insteon fans via local hub control.
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/fan.insteon_local/ https://home-assistant.io/components/fan.insteon_local/
""" """
import json
import logging import logging
import os
from datetime import timedelta from datetime import timedelta
from homeassistant.components.fan import ( from homeassistant.components.fan import (
@ -14,6 +12,7 @@ from homeassistant.components.fan import (
SUPPORT_SET_SPEED, FanEntity) SUPPORT_SET_SPEED, FanEntity)
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
import homeassistant.util as util import homeassistant.util as util
from homeassistant.util.json import load_json, save_json
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -33,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Insteon local fan platform.""" """Set up the Insteon local fan platform."""
insteonhub = hass.data['insteon_local'] insteonhub = hass.data['insteon_local']
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF)) conf_fans = load_json(hass.config.path(INSTEON_LOCAL_FANS_CONF))
if conf_fans: if conf_fans:
for device_id in conf_fans: for device_id in conf_fans:
setup_fan(device_id, conf_fans[device_id], insteonhub, hass, setup_fan(device_id, conf_fans[device_id], insteonhub, hass,
@ -88,44 +87,16 @@ def setup_fan(device_id, name, insteonhub, hass, add_devices_callback):
configurator.request_done(request_id) configurator.request_done(request_id)
_LOGGER.info("Device configuration done!") _LOGGER.info("Device configuration done!")
conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF)) conf_fans = load_json(hass.config.path(INSTEON_LOCAL_FANS_CONF))
if device_id not in conf_fans: if device_id not in conf_fans:
conf_fans[device_id] = name conf_fans[device_id] = name
if not config_from_file( save_json(hass.config.path(INSTEON_LOCAL_FANS_CONF), conf_fans)
hass.config.path(INSTEON_LOCAL_FANS_CONF),
conf_fans):
_LOGGER.error("Failed to save configuration file")
device = insteonhub.fan(device_id) device = insteonhub.fan(device_id)
add_devices_callback([InsteonLocalFanDevice(device, name)]) add_devices_callback([InsteonLocalFanDevice(device, name)])
def config_from_file(filename, config=None):
"""Small configuration file management function."""
if config:
# We're writing configuration
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(config))
except IOError as error:
_LOGGER.error('Saving config file failed: %s', error)
return False
return True
else:
# We're reading config
if os.path.isfile(filename):
try:
with open(filename, 'r') as fdesc:
return json.loads(fdesc.read())
except IOError as error:
_LOGGER.error("Reading configuration file failed: %s", error)
# This won't work yet
return False
else:
return {}
class InsteonLocalFanDevice(FanEntity): class InsteonLocalFanDevice(FanEntity):
"""An abstract Class for an Insteon node.""" """An abstract Class for an Insteon node."""

View File

@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}) })
REQUIREMENTS = ['python-miio==0.3.1'] REQUIREMENTS = ['python-miio==0.3.2']
ATTR_TEMPERATURE = 'temperature' ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity' ATTR_HUMIDITY = 'humidity'

View File

@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20171121.0'] REQUIREMENTS = ['home-assistant-frontend==20171130.0', 'user-agents==1.1.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@ -32,6 +32,7 @@ URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
CONF_THEMES = 'themes' CONF_THEMES = 'themes'
CONF_EXTRA_HTML_URL = 'extra_html_url' CONF_EXTRA_HTML_URL = 'extra_html_url'
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
CONF_FRONTEND_REPO = 'development_repo' CONF_FRONTEND_REPO = 'development_repo'
CONF_JS_VERSION = 'javascript_version' CONF_JS_VERSION = 'javascript_version'
JS_DEFAULT_OPTION = 'es5' JS_DEFAULT_OPTION = 'es5'
@ -63,6 +64,7 @@ DATA_FINALIZE_PANEL = 'frontend_finalize_panel'
DATA_PANELS = 'frontend_panels' DATA_PANELS = 'frontend_panels'
DATA_JS_VERSION = 'frontend_js_version' DATA_JS_VERSION = 'frontend_js_version'
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url' DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
DATA_EXTRA_HTML_URL_ES5 = 'frontend_extra_html_url_es5'
DATA_THEMES = 'frontend_themes' DATA_THEMES = 'frontend_themes'
DATA_DEFAULT_THEME = 'frontend_default_theme' DATA_DEFAULT_THEME = 'frontend_default_theme'
DEFAULT_THEME = 'default' DEFAULT_THEME = 'default'
@ -79,6 +81,8 @@ CONFIG_SCHEMA = vol.Schema({
}), }),
vol.Optional(CONF_EXTRA_HTML_URL): vol.Optional(CONF_EXTRA_HTML_URL):
vol.All(cv.ensure_list, [cv.string]), vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EXTRA_HTML_URL_ES5):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_JS_VERSION, default=JS_DEFAULT_OPTION): vol.Optional(CONF_JS_VERSION, default=JS_DEFAULT_OPTION):
vol.In(JS_OPTIONS) vol.In(JS_OPTIONS)
}), }),
@ -269,11 +273,12 @@ def async_register_panel(hass, component_name, path, md5=None,
@bind_hass @bind_hass
@callback @callback
def add_extra_html_url(hass, url): def add_extra_html_url(hass, url, es5=False):
"""Register extra html url to load.""" """Register extra html url to load."""
url_set = hass.data.get(DATA_EXTRA_HTML_URL) key = DATA_EXTRA_HTML_URL_ES5 if es5 else DATA_EXTRA_HTML_URL
url_set = hass.data.get(key)
if url_set is None: if url_set is None:
url_set = hass.data[DATA_EXTRA_HTML_URL] = set() url_set = hass.data[key] = set()
url_set.add(url) url_set.add(url)
@ -358,9 +363,13 @@ def async_setup(hass, config):
if DATA_EXTRA_HTML_URL not in hass.data: if DATA_EXTRA_HTML_URL not in hass.data:
hass.data[DATA_EXTRA_HTML_URL] = set() hass.data[DATA_EXTRA_HTML_URL] = set()
if DATA_EXTRA_HTML_URL_ES5 not in hass.data:
hass.data[DATA_EXTRA_HTML_URL_ES5] = set()
for url in conf.get(CONF_EXTRA_HTML_URL, []): for url in conf.get(CONF_EXTRA_HTML_URL, []):
add_extra_html_url(hass, url) add_extra_html_url(hass, url, False)
for url in conf.get(CONF_EXTRA_HTML_URL_ES5, []):
add_extra_html_url(hass, url, True)
yield from async_setup_themes(hass, conf.get(CONF_THEMES)) yield from async_setup_themes(hass, conf.get(CONF_THEMES))
@ -467,7 +476,8 @@ class IndexView(HomeAssistantView):
def get(self, request, extra=None): def get(self, request, extra=None):
"""Serve the index view.""" """Serve the index view."""
hass = request.app['hass'] hass = request.app['hass']
latest = _is_latest(self.js_option, request) latest = self.repo_path is not None or \
_is_latest(self.js_option, request)
if request.path == '/': if request.path == '/':
panel = 'states' panel = 'states'
@ -481,21 +491,21 @@ class IndexView(HomeAssistantView):
else: else:
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5 panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5
no_auth = 'true' no_auth = '1'
if hass.config.api.api_password and not is_trusted_ip(request): if hass.config.api.api_password and not is_trusted_ip(request):
# do not try to auto connect on load # do not try to auto connect on load
no_auth = 'false' no_auth = '0'
template = yield from hass.async_add_job(self.get_template, latest) template = yield from hass.async_add_job(self.get_template, latest)
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
resp = template.render( resp = template.render(
no_auth=no_auth, no_auth=no_auth,
panel_url=panel_url, panel_url=panel_url,
panels=hass.data[DATA_PANELS], panels=hass.data[DATA_PANELS],
dev_mode=self.repo_path is not None,
theme_color=MANIFEST_JSON['theme_color'], theme_color=MANIFEST_JSON['theme_color'],
extra_urls=hass.data[DATA_EXTRA_HTML_URL], extra_urls=hass.data[extra_key],
latest=latest,
) )
return web.Response(text=resp, content_type='text/html') return web.Response(text=resp, content_type='text/html')
@ -547,10 +557,36 @@ def _is_latest(js_option, request):
""" """
if request is None: if request is None:
return js_option == 'latest' return js_option == 'latest'
latest_in_query = 'latest' in request.query or (
request.headers.get('Referer') and # latest in query
'latest' in urlparse(request.headers['Referer']).query) if 'latest' in request.query or (
es5_in_query = 'es5' in request.query or ( request.headers.get('Referer') and
request.headers.get('Referer') and 'latest' in urlparse(request.headers['Referer']).query):
'es5' in urlparse(request.headers['Referer']).query) return True
return latest_in_query or (not es5_in_query and js_option == 'latest')
# es5 in query
if 'es5' in request.query or (
request.headers.get('Referer') and
'es5' in urlparse(request.headers['Referer']).query):
return False
# non-auto option in config
if js_option != 'auto':
return js_option == 'latest'
from user_agents import parse
useragent = parse(request.headers.get('User-Agent'))
# on iOS every browser is a Safari which we support from version 10.
if useragent.os.family == 'iOS':
return useragent.os.version[0] >= 10
family_min_version = {
'Chrome': 50, # Probably can reduce this
'Firefox': 41, # Destructuring added in 41
'Opera': 40, # Probably can reduce this
'Edge': 14, # Maybe can reduce this
'Safari': 10, # many features not supported by 9
}
version = family_min_version.get(useragent.browser.family)
return version and useragent.browser.version[0] >= version

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -126,21 +126,23 @@ class GoogleAssistantView(HomeAssistantView):
commands = [] commands = []
for command in requested_commands: for command in requested_commands:
ent_ids = [ent.get('id') for ent in command.get('devices', [])] ent_ids = [ent.get('id') for ent in command.get('devices', [])]
execution = command.get('execution')[0] for execution in command.get('execution'):
for eid in ent_ids: for eid in ent_ids:
success = False success = False
domain = eid.split('.')[0] domain = eid.split('.')[0]
(service, service_data) = determine_service( (service, service_data) = determine_service(
eid, execution.get('command'), execution.get('params'), eid, execution.get('command'), execution.get('params'),
hass.config.units) hass.config.units)
success = yield from hass.services.async_call( if domain == "group":
domain, service, service_data, blocking=True) domain = "homeassistant"
result = {"ids": [eid], "states": {}} success = yield from hass.services.async_call(
if success: domain, service, service_data, blocking=True)
result['status'] = 'SUCCESS' result = {"ids": [eid], "states": {}}
else: if success:
result['status'] = 'ERROR' result['status'] = 'SUCCESS'
commands.append(result) else:
result['status'] = 'ERROR'
commands.append(result)
return self.json( return self.json(
_make_actions_response(request_id, {'commands': commands})) _make_actions_response(request_id, {'commands': commands}))

View File

@ -39,7 +39,7 @@ _LOGGER = logging.getLogger(__name__)
# Mapping is [actions schema, primary trait, optional features] # Mapping is [actions schema, primary trait, optional features]
# optional is SUPPORT_* = (trait, command) # optional is SUPPORT_* = (trait, command)
MAPPING_COMPONENT = { MAPPING_COMPONENT = {
group.DOMAIN: [TYPE_SCENE, TRAIT_SCENE, None], group.DOMAIN: [TYPE_SWITCH, TRAIT_ONOFF, None],
scene.DOMAIN: [TYPE_SCENE, TRAIT_SCENE, None], scene.DOMAIN: [TYPE_SCENE, TRAIT_SCENE, None],
script.DOMAIN: [TYPE_SCENE, TRAIT_SCENE, None], script.DOMAIN: [TYPE_SCENE, TRAIT_SCENE, None],
switch.DOMAIN: [TYPE_SWITCH, TRAIT_ONOFF, None], switch.DOMAIN: [TYPE_SWITCH, TRAIT_ONOFF, None],
@ -94,10 +94,11 @@ def entity_to_device(entity: Entity, units: UnitSystem):
# use aliases # use aliases
aliases = entity.attributes.get(CONF_ALIASES) aliases = entity.attributes.get(CONF_ALIASES)
if isinstance(aliases, list): if aliases:
device['name']['nicknames'] = aliases if isinstance(aliases, list):
else: device['name']['nicknames'] = aliases
_LOGGER.warning("%s must be a list", CONF_ALIASES) else:
_LOGGER.warning("%s must be a list", CONF_ALIASES)
# add trait if entity supports feature # add trait if entity supports feature
if class_data[2]: if class_data[2]:
@ -124,14 +125,15 @@ def entity_to_device(entity: Entity, units: UnitSystem):
if entity.domain == climate.DOMAIN: if entity.domain == climate.DOMAIN:
modes = ','.join( modes = ','.join(
m for m in entity.attributes.get(climate.ATTR_OPERATION_LIST, []) m.lower() for m in entity.attributes.get(
if m in CLIMATE_SUPPORTED_MODES) climate.ATTR_OPERATION_LIST, [])
if m.lower() in CLIMATE_SUPPORTED_MODES)
device['attributes'] = { device['attributes'] = {
'availableThermostatModes': modes, 'availableThermostatModes': modes,
'thermostatTemperatureUnit': 'thermostatTemperatureUnit':
'F' if units.temperature_unit == TEMP_FAHRENHEIT else 'C', 'F' if units.temperature_unit == TEMP_FAHRENHEIT else 'C',
} }
_LOGGER.debug('Thermostat attributes %s', device['attributes'])
return device return device
@ -143,7 +145,7 @@ def query_device(entity: Entity, units: UnitSystem) -> dict:
return None return None
return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1) return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
if entity.domain == climate.DOMAIN: if entity.domain == climate.DOMAIN:
mode = entity.attributes.get(climate.ATTR_OPERATION_MODE) mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower()
if mode not in CLIMATE_SUPPORTED_MODES: if mode not in CLIMATE_SUPPORTED_MODES:
mode = 'on' mode = 'on'
response = { response = {
@ -218,6 +220,7 @@ def determine_service(
Attempt to return a tuple of service and service_data based on the entity Attempt to return a tuple of service and service_data based on the entity
and action requested. and action requested.
""" """
_LOGGER.debug("Handling command %s with data %s", command, params)
domain = entity_id.split('.')[0] domain = entity_id.split('.')[0]
service_data = {ATTR_ENTITY_ID: entity_id} # type: Dict[str, Any] service_data = {ATTR_ENTITY_ID: entity_id} # type: Dict[str, Any]
# special media_player handling # special media_player handling
@ -260,7 +263,6 @@ def determine_service(
service_data['brightness'] = int(brightness / 100 * 255) service_data['brightness'] = int(brightness / 100 * 255)
return (SERVICE_TURN_ON, service_data) return (SERVICE_TURN_ON, service_data)
_LOGGER.debug("Handling command %s with data %s", command, params)
if command == COMMAND_COLOR: if command == COMMAND_COLOR:
color_data = params.get('color') color_data = params.get('color')
if color_data is not None: if color_data is not None:

View File

@ -0,0 +1,80 @@
"""
Support for the Hive devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/hive/
"""
import logging
import voluptuous as vol
from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL,
CONF_USERNAME)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
REQUIREMENTS = ['pyhiveapi==0.2.5']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'hive'
DATA_HIVE = 'data_hive'
DEVICETYPES = {
'binary_sensor': 'device_list_binary_sensor',
'climate': 'device_list_climate',
'light': 'device_list_light',
'switch': 'device_list_plug',
'sensor': 'device_list_sensor',
}
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=2): cv.positive_int,
})
}, extra=vol.ALLOW_EXTRA)
class HiveSession:
"""Initiate Hive Session Class."""
entities = []
core = None
heating = None
hotwater = None
light = None
sensor = None
switch = None
def setup(hass, config):
"""Set up the Hive Component."""
from pyhiveapi import Pyhiveapi
session = HiveSession()
session.core = Pyhiveapi()
username = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
update_interval = config[DOMAIN][CONF_SCAN_INTERVAL]
devicelist = session.core.initialise_api(username,
password,
update_interval)
if devicelist is None:
_LOGGER.error("Hive API initialization failed")
return False
session.sensor = Pyhiveapi.Sensor()
session.heating = Pyhiveapi.Heating()
session.hotwater = Pyhiveapi.Hotwater()
session.light = Pyhiveapi.Light()
session.switch = Pyhiveapi.Switch()
hass.data[DATA_HIVE] = session
for ha_type, hive_type in DEVICETYPES.items():
for key, devices in devicelist.items():
if key == hive_type:
for hivedevice in devices:
load_platform(hass, ha_type, DOMAIN, hivedevice, config)
return True

View File

@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.event import track_time_interval
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['pyhomematic==0.1.34'] REQUIREMENTS = ['pyhomematic==0.1.35']
DOMAIN = 'homematic' DOMAIN = 'homematic'
@ -56,7 +56,7 @@ SERVICE_SET_DEV_VALUE = 'set_dev_value'
HM_DEVICE_TYPES = { HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: [ DISCOVER_SWITCHES: [
'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren',
'IPSwitchPowermeter', 'KeyMatic', 'HMWIOSwitch', 'Rain', 'EcoLogic'], 'IPSwitchPowermeter', 'KeyMatic', 'HMWIOSwitch', 'Rain', 'EcoLogic'],
DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'], DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'],
DISCOVER_SENSORS: [ DISCOVER_SENSORS: [
@ -66,7 +66,7 @@ HM_DEVICE_TYPES = {
'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor', 'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor',
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch', 'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
'IPSmoke'], 'IPSmoke', 'RFSiren', 'PresenceIP'],
DISCOVER_CLIMATE: [ DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
@ -74,7 +74,8 @@ HM_DEVICE_TYPES = {
DISCOVER_BINARY_SENSORS: [ DISCOVER_BINARY_SENSORS: [
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2', 'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact',
'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor'], 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor',
'PresenceIP'],
DISCOVER_COVER: ['Blind', 'KeyBlind'] DISCOVER_COVER: ['Blind', 'KeyBlind']
} }

View File

@ -4,6 +4,8 @@ A component which allows you to send data to an Influx database.
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/influxdb/ https://home-assistant.io/components/influxdb/
""" """
from datetime import timedelta
from functools import wraps, partial
import logging import logging
import re import re
@ -16,6 +18,7 @@ from homeassistant.const import (
CONF_EXCLUDE, CONF_INCLUDE, CONF_DOMAINS, CONF_ENTITIES) CONF_EXCLUDE, CONF_INCLUDE, CONF_DOMAINS, CONF_ENTITIES)
from homeassistant.helpers import state as state_helper from homeassistant.helpers import state as state_helper
from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.entity_values import EntityValues
from homeassistant.util import utcnow
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['influxdb==4.1.1'] REQUIREMENTS = ['influxdb==4.1.1']
@ -30,6 +33,8 @@ CONF_TAGS_ATTRIBUTES = 'tags_attributes'
CONF_COMPONENT_CONFIG = 'component_config' CONF_COMPONENT_CONFIG = 'component_config'
CONF_COMPONENT_CONFIG_GLOB = 'component_config_glob' CONF_COMPONENT_CONFIG_GLOB = 'component_config_glob'
CONF_COMPONENT_CONFIG_DOMAIN = 'component_config_domain' CONF_COMPONENT_CONFIG_DOMAIN = 'component_config_domain'
CONF_RETRY_COUNT = 'max_retries'
CONF_RETRY_QUEUE = 'retry_queue_limit'
DEFAULT_DATABASE = 'home_assistant' DEFAULT_DATABASE = 'home_assistant'
DEFAULT_VERIFY_SSL = True DEFAULT_VERIFY_SSL = True
@ -58,6 +63,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string,
vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_SSL): cv.boolean, vol.Optional(CONF_SSL): cv.boolean,
vol.Optional(CONF_RETRY_COUNT, default=0): cv.positive_int,
vol.Optional(CONF_RETRY_QUEUE, default=20): cv.positive_int,
vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string, vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string,
vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string, vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string,
vol.Optional(CONF_TAGS, default={}): vol.Optional(CONF_TAGS, default={}):
@ -119,6 +126,8 @@ def setup(hass, config):
conf[CONF_COMPONENT_CONFIG], conf[CONF_COMPONENT_CONFIG],
conf[CONF_COMPONENT_CONFIG_DOMAIN], conf[CONF_COMPONENT_CONFIG_DOMAIN],
conf[CONF_COMPONENT_CONFIG_GLOB]) conf[CONF_COMPONENT_CONFIG_GLOB])
max_tries = conf.get(CONF_RETRY_COUNT)
queue_limit = conf.get(CONF_RETRY_QUEUE)
try: try:
influx = InfluxDBClient(**kwargs) influx = InfluxDBClient(**kwargs)
@ -145,12 +154,18 @@ def setup(hass, config):
(whitelist_d and state.domain not in whitelist_d): (whitelist_d and state.domain not in whitelist_d):
return return
_state = float(state_helper.state_as_number(state)) _include_state = _include_value = False
_state_key = "value"
except ValueError:
_state = state.state
_state_key = "state"
_state_as_value = float(state.state)
_include_value = True
except ValueError:
try:
_state_as_value = float(state_helper.state_as_number(state))
_include_state = _include_value = True
except ValueError:
_include_state = True
include_uom = True
measurement = component_config.get(state.entity_id).get( measurement = component_config.get(state.entity_id).get(
CONF_OVERRIDE_MEASUREMENT) CONF_OVERRIDE_MEASUREMENT)
if measurement in (None, ''): if measurement in (None, ''):
@ -163,6 +178,8 @@ def setup(hass, config):
measurement = default_measurement measurement = default_measurement
else: else:
measurement = state.entity_id measurement = state.entity_id
else:
include_uom = False
json_body = [ json_body = [
{ {
@ -173,15 +190,18 @@ def setup(hass, config):
}, },
'time': event.time_fired, 'time': event.time_fired,
'fields': { 'fields': {
_state_key: _state,
} }
} }
] ]
if _include_state:
json_body[0]['fields']['state'] = state.state
if _include_value:
json_body[0]['fields']['value'] = _state_as_value
for key, value in state.attributes.items(): for key, value in state.attributes.items():
if key in tags_attributes: if key in tags_attributes:
json_body[0]['tags'][key] = value json_body[0]['tags'][key] = value
elif key != 'unit_of_measurement': elif key != 'unit_of_measurement' or include_uom:
# If the key is already in fields # If the key is already in fields
if key in json_body[0]['fields']: if key in json_body[0]['fields']:
key = key + "_" key = key + "_"
@ -202,6 +222,11 @@ def setup(hass, config):
json_body[0]['tags'].update(tags) json_body[0]['tags'].update(tags)
_write_data(json_body)
@RetryOnError(hass, retry_limit=max_tries, retry_delay=20,
queue_limit=queue_limit)
def _write_data(json_body):
try: try:
influx.write_points(json_body) influx.write_points(json_body)
except exceptions.InfluxDBClientError: except exceptions.InfluxDBClientError:
@ -210,3 +235,79 @@ def setup(hass, config):
hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener) hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener)
return True return True
class RetryOnError(object):
"""A class for retrying a failed task a certain amount of tries.
This method decorator makes a method retrying on errors. If there was an
uncaught exception, it schedules another try to execute the task after a
retry delay. It does this up to the maximum number of retries.
It can be used for all probable "self-healing" problems like network
outages. The task will be rescheduled using HAs scheduling mechanism.
It takes a Hass instance, a maximum number of retries and a retry delay
in seconds as arguments.
The queue limit defines the maximum number of calls that are allowed to
be queued at a time. If this number is reached, every new call discards
an old one.
"""
def __init__(self, hass, retry_limit=0, retry_delay=20, queue_limit=100):
"""Initialize the decorator."""
self.hass = hass
self.retry_limit = retry_limit
self.retry_delay = timedelta(seconds=retry_delay)
self.queue_limit = queue_limit
def __call__(self, method):
"""Decorate the target method."""
from homeassistant.helpers.event import track_point_in_utc_time
@wraps(method)
def wrapper(*args, **kwargs):
"""Wrapped method."""
# pylint: disable=protected-access
if not hasattr(wrapper, "_retry_queue"):
wrapper._retry_queue = []
def scheduled(retry=0, untrack=None, event=None):
"""Call the target method.
It is called directly at the first time and then called
scheduled within the Hass mainloop.
"""
if untrack is not None:
wrapper._retry_queue.remove(untrack)
# pylint: disable=broad-except
try:
method(*args, **kwargs)
except Exception as ex:
if retry == self.retry_limit:
raise
if len(wrapper._retry_queue) >= self.queue_limit:
last = wrapper._retry_queue.pop(0)
if 'remove' in last:
func = last['remove']
func()
if 'exc' in last:
_LOGGER.error(
"Retry queue overflow, drop oldest entry: %s",
str(last['exc']))
target = utcnow() + self.retry_delay
tracking = {'target': target}
remove = track_point_in_utc_time(self.hass,
partial(scheduled,
retry + 1,
tracking),
target)
tracking['remove'] = remove
tracking["exc"] = ex
wrapper._retry_queue.append(tracking)
scheduled()
return wrapper

View File

@ -5,26 +5,21 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/ecosystem/ios/ https://home-assistant.io/ecosystem/ios/
""" """
import asyncio import asyncio
import os
import json
import logging import logging
import datetime import datetime
import voluptuous as vol import voluptuous as vol
# from voluptuous.humanize import humanize_error # from voluptuous.humanize import humanize_error
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.remote import JSONEncoder
from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR, from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR,
HTTP_BAD_REQUEST) HTTP_BAD_REQUEST)
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.util.json import load_json, save_json
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -174,36 +169,6 @@ CONFIG_FILE = {ATTR_DEVICES: {}}
CONFIG_FILE_PATH = "" CONFIG_FILE_PATH = ""
def _load_config(filename):
"""Load configuration."""
if not os.path.isfile(filename):
return {}
try:
with open(filename, "r") as fdesc:
inp = fdesc.read()
# In case empty file
if not inp:
return {}
return json.loads(inp)
except (IOError, ValueError) as error:
_LOGGER.error("Reading config file %s failed: %s", filename, error)
return None
def _save_config(filename, config):
"""Save configuration."""
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(config, cls=JSONEncoder))
except (IOError, TypeError) as error:
_LOGGER.error("Saving config file failed: %s", error)
return False
return True
def devices_with_push(): def devices_with_push():
"""Return a dictionary of push enabled targets.""" """Return a dictionary of push enabled targets."""
targets = {} targets = {}
@ -244,7 +209,7 @@ def setup(hass, config):
CONFIG_FILE_PATH = hass.config.path(CONFIGURATION_FILE) CONFIG_FILE_PATH = hass.config.path(CONFIGURATION_FILE)
CONFIG_FILE = _load_config(CONFIG_FILE_PATH) CONFIG_FILE = load_json(CONFIG_FILE_PATH)
if CONFIG_FILE == {}: if CONFIG_FILE == {}:
CONFIG_FILE[ATTR_DEVICES] = {} CONFIG_FILE[ATTR_DEVICES] = {}
@ -305,7 +270,9 @@ class iOSIdentifyDeviceView(HomeAssistantView):
CONFIG_FILE[ATTR_DEVICES][name] = data CONFIG_FILE[ATTR_DEVICES][name] = data
if not _save_config(CONFIG_FILE_PATH, CONFIG_FILE): try:
save_json(CONFIG_FILE_PATH, CONFIG_FILE)
except HomeAssistantError:
return self.json_message("Error saving device.", return self.json_message("Error saving device.",
HTTP_INTERNAL_SERVER_ERROR) HTTP_INTERNAL_SERVER_ERROR)

View File

@ -37,19 +37,22 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
add_devices([BlinktLight(blinkt, name)]) add_devices([
BlinktLight(blinkt, name, index) for index in range(blinkt.NUM_PIXELS)
])
class BlinktLight(Light): class BlinktLight(Light):
"""Representation of a Blinkt! Light.""" """Representation of a Blinkt! Light."""
def __init__(self, blinkt, name): def __init__(self, blinkt, name, index):
"""Initialize a Blinkt Light. """Initialize a Blinkt Light.
Default brightness and white color. Default brightness and white color.
""" """
self._blinkt = blinkt self._blinkt = blinkt
self._name = name self._name = "{}_{}".format(name, index)
self._index = index
self._is_on = False self._is_on = False
self._brightness = 255 self._brightness = 255
self._rgb_color = [255, 255, 255] self._rgb_color = [255, 255, 255]
@ -103,10 +106,11 @@ class BlinktLight(Light):
self._brightness = kwargs[ATTR_BRIGHTNESS] self._brightness = kwargs[ATTR_BRIGHTNESS]
percent_bright = (self._brightness / 255) percent_bright = (self._brightness / 255)
self._blinkt.set_all(self._rgb_color[0], self._blinkt.set_pixel(self._index,
self._rgb_color[1], self._rgb_color[0],
self._rgb_color[2], self._rgb_color[1],
percent_bright) self._rgb_color[2],
percent_bright)
self._blinkt.show() self._blinkt.show()
@ -115,7 +119,7 @@ class BlinktLight(Light):
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Instruct the light to turn off.""" """Instruct the light to turn off."""
self._blinkt.set_brightness(0) self._blinkt.set_pixel(self._index, 0, 0, 0, 0)
self._blinkt.show() self._blinkt.show()
self._is_on = False self._is_on = False
self.schedule_update_ha_state() self.schedule_update_ha_state()

View File

@ -0,0 +1,126 @@
"""
Support for the Hive devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.hive/
"""
from homeassistant.components.hive import DATA_HIVE
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP,
SUPPORT_RGB_COLOR, Light)
DEPENDENCIES = ['hive']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Hive light devices."""
if discovery_info is None:
return
session = hass.data.get(DATA_HIVE)
add_devices([HiveDeviceLight(session, discovery_info)])
class HiveDeviceLight(Light):
"""Hive Active Light Device."""
def __init__(self, hivesession, hivedevice):
"""Initialize the Light device."""
self.node_id = hivedevice["Hive_NodeID"]
self.node_name = hivedevice["Hive_NodeName"]
self.device_type = hivedevice["HA_DeviceType"]
self.light_device_type = hivedevice["Hive_Light_DeviceType"]
self.session = hivesession
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
self.session.entities.append(self)
def handle_update(self, updatesource):
"""Handle the new update request."""
if '{}.{}'.format(self.device_type, self.node_id) not in updatesource:
self.schedule_update_ha_state()
@property
def name(self):
"""Return the display name of this light."""
return self.node_name
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
if self.light_device_type == "tuneablelight" \
or self.light_device_type == "colourtuneablelight":
return self.session.light.get_min_colour_temp(self.node_id)
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
if self.light_device_type == "tuneablelight" \
or self.light_device_type == "colourtuneablelight":
return self.session.light.get_max_colour_temp(self.node_id)
@property
def color_temp(self):
"""Return the CT color value in mireds."""
if self.light_device_type == "tuneablelight" \
or self.light_device_type == "colourtuneablelight":
return self.session.light.get_color_temp(self.node_id)
@property
def brightness(self):
"""Brightness of the light (an integer in the range 1-255)."""
return self.session.light.get_brightness(self.node_id)
@property
def is_on(self):
"""Return true if light is on."""
return self.session.light.get_state(self.node_id)
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
new_brightness = None
new_color_temp = None
if ATTR_BRIGHTNESS in kwargs:
tmp_new_brightness = kwargs.get(ATTR_BRIGHTNESS)
percentage_brightness = ((tmp_new_brightness / 255) * 100)
new_brightness = int(round(percentage_brightness / 5.0) * 5.0)
if new_brightness == 0:
new_brightness = 5
if ATTR_COLOR_TEMP in kwargs:
tmp_new_color_temp = kwargs.get(ATTR_COLOR_TEMP)
new_color_temp = round(1000000 / tmp_new_color_temp)
if new_brightness is not None:
self.session.light.set_brightness(self.node_id, new_brightness)
elif new_color_temp is not None:
self.session.light.set_colour_temp(self.node_id, new_color_temp)
else:
self.session.light.turn_on(self.node_id)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
def turn_off(self):
"""Instruct the light to turn off."""
self.session.light.turn_off(self.node_id)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
@property
def supported_features(self):
"""Flag supported features."""
supported_features = None
if self.light_device_type == "warmwhitelight":
supported_features = SUPPORT_BRIGHTNESS
elif self.light_device_type == "tuneablelight":
supported_features = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP)
elif self.light_device_type == "colourtuneablelight":
supported_features = (
SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR)
return supported_features
def update(self):
"""Update all Node data frome Hive."""
self.session.core.update_data(self.node_id)

View File

@ -83,7 +83,12 @@ SCENE_SCHEMA = vol.Schema({
}) })
ATTR_IS_HUE_GROUP = "is_hue_group" ATTR_IS_HUE_GROUP = "is_hue_group"
GROUP_NAME_ALL_HUE_LIGHTS = "All Hue Lights"
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)
"""
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
@ -204,21 +209,6 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable,
_LOGGER.error("Got unexpected result from Hue API") _LOGGER.error("Got unexpected result from Hue API")
return return
if not skip_groups:
# Group ID 0 is a special group in the hub for all lights, but it
# is not returned by get_api() so explicitly get it and include it.
# See https://developers.meethue.com/documentation/
# groups-api#21_get_all_groups
_LOGGER.debug("Getting group 0 from bridge")
all_lights = bridge.get_group(0)
if not isinstance(all_lights, dict):
_LOGGER.error("Got unexpected result from Hue API for group 0")
return
# Hue hub returns name of group 0 as "Group 0", so rename
# for ease of use in HA.
all_lights['name'] = GROUP_NAME_ALL_HUE_LIGHTS
api_groups["0"] = all_lights
new_lights = [] new_lights = []
api_name = api.get('config').get('name') api_name = api.get('config').get('name')
@ -298,10 +288,8 @@ def request_configuration(host, hass, add_devices, filename,
_CONFIGURING[host] = configurator.request_config( _CONFIGURING[host] = configurator.request_config(
"Philips Hue", hue_configuration_callback, "Philips Hue", hue_configuration_callback,
description=("Press the button on the bridge to register Philips Hue " description=CONFIG_INSTRUCTIONS,
"with Home Assistant."),
entity_picture="/static/images/logo_philips_hue.png", entity_picture="/static/images/logo_philips_hue.png",
description_image="/static/images/config_philips_hue.jpg",
submit_caption="I have pressed the button" submit_caption="I have pressed the button"
) )

View File

@ -4,14 +4,14 @@ Support for Insteon dimmers via local hub control.
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/light.insteon_local/ https://home-assistant.io/components/light.insteon_local/
""" """
import json
import logging import logging
import os
from datetime import timedelta from datetime import timedelta
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
import homeassistant.util as util import homeassistant.util as util
from homeassistant.util.json import load_json, save_json
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -31,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Insteon local light platform.""" """Set up the Insteon local light platform."""
insteonhub = hass.data['insteon_local'] insteonhub = hass.data['insteon_local']
conf_lights = config_from_file(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF)) conf_lights = load_json(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF))
if conf_lights: if conf_lights:
for device_id in conf_lights: for device_id in conf_lights:
setup_light(device_id, conf_lights[device_id], insteonhub, hass, setup_light(device_id, conf_lights[device_id], insteonhub, hass,
@ -85,44 +85,16 @@ def setup_light(device_id, name, insteonhub, hass, add_devices_callback):
configurator.request_done(request_id) configurator.request_done(request_id)
_LOGGER.debug("Device configuration done") _LOGGER.debug("Device configuration done")
conf_lights = config_from_file(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF)) conf_lights = load_json(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF))
if device_id not in conf_lights: if device_id not in conf_lights:
conf_lights[device_id] = name conf_lights[device_id] = name
if not config_from_file( save_json(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF), conf_lights)
hass.config.path(INSTEON_LOCAL_LIGHTS_CONF),
conf_lights):
_LOGGER.error("Failed to save configuration file")
device = insteonhub.dimmer(device_id) device = insteonhub.dimmer(device_id)
add_devices_callback([InsteonLocalDimmerDevice(device, name)]) add_devices_callback([InsteonLocalDimmerDevice(device, name)])
def config_from_file(filename, config=None):
"""Small configuration file management function."""
if config:
# We're writing configuration
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(config))
except IOError as error:
_LOGGER.error("Saving config file failed: %s", error)
return False
return True
else:
# We're reading config
if os.path.isfile(filename):
try:
with open(filename, 'r') as fdesc:
return json.loads(fdesc.read())
except IOError as error:
_LOGGER.error("Reading configuration file failed: %s", error)
# This won't work yet
return False
else:
return {}
class InsteonLocalDimmerDevice(Light): class InsteonLocalDimmerDevice(Light):
"""An abstract Class for an Insteon node.""" """An abstract Class for an Insteon node."""

View File

@ -120,6 +120,7 @@ class TradfriGroup(Light):
@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
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,
@ -279,6 +280,7 @@ class TradfriLight(Light):
@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
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,

View File

@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}) })
REQUIREMENTS = ['python-miio==0.3.1'] REQUIREMENTS = ['python-miio==0.3.2']
# The light does not accept cct values < 1 # The light does not accept cct values < 1
CCT_MIN = 1 CCT_MIN = 1

View File

@ -222,7 +222,8 @@ class YeelightLight(Light):
color_mode = int(color_mode) color_mode = int(color_mode)
if color_mode == 2: # color temperature if color_mode == 2: # color temperature
return color_temperature_to_rgb(self.color_temp) temp_in_k = mired_to_kelvin(self._color_temp)
return color_temperature_to_rgb(temp_in_k)
if color_mode == 3: # hsv if color_mode == 3: # hsv
hue = int(self._properties.get('hue')) hue = int(self._properties.get('hue'))
sat = int(self._properties.get('sat')) sat = int(self._properties.get('sat'))

View File

@ -16,7 +16,7 @@ from homeassistant.components.media_player import (
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2017.11.15'] REQUIREMENTS = ['youtube_dl==2017.11.26']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -5,8 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.braviatv/ https://home-assistant.io/components/media_player.braviatv/
""" """
import logging import logging
import os
import json
import re import re
import voluptuous as vol import voluptuous as vol
@ -18,6 +16,7 @@ from homeassistant.components.media_player import (
PLATFORM_SCHEMA) PLATFORM_SCHEMA)
from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
REQUIREMENTS = [ REQUIREMENTS = [
'https://github.com/aparraga/braviarc/archive/0.3.7.zip' 'https://github.com/aparraga/braviarc/archive/0.3.7.zip'
@ -61,38 +60,6 @@ def _get_mac_address(ip_address):
return None return None
def _config_from_file(filename, config=None):
"""Create the configuration from a file."""
if config:
# We're writing configuration
bravia_config = _config_from_file(filename)
if bravia_config is None:
bravia_config = {}
new_config = bravia_config.copy()
new_config.update(config)
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(new_config))
except IOError as error:
_LOGGER.error("Saving config file failed: %s", error)
return False
return True
else:
# We're reading config
if os.path.isfile(filename):
try:
with open(filename, 'r') as fdesc:
return json.loads(fdesc.read())
except ValueError as error:
return {}
except IOError as error:
_LOGGER.error("Reading config file failed: %s", error)
# This won't work yet
return False
else:
return {}
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Sony Bravia TV platform.""" """Set up the Sony Bravia TV platform."""
@ -102,7 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return return
pin = None pin = None
bravia_config = _config_from_file(hass.config.path(BRAVIA_CONFIG_FILE)) bravia_config = load_json(hass.config.path(BRAVIA_CONFIG_FILE))
while bravia_config: while bravia_config:
# Set up a configured TV # Set up a configured TV
host_ip, host_config = bravia_config.popitem() host_ip, host_config = bravia_config.popitem()
@ -136,10 +103,9 @@ def setup_bravia(config, pin, hass, add_devices):
_LOGGER.info("Discovery configuration done") _LOGGER.info("Discovery configuration done")
# Save config # Save config
if not _config_from_file( save_json(
hass.config.path(BRAVIA_CONFIG_FILE), hass.config.path(BRAVIA_CONFIG_FILE),
{host: {'pin': pin, 'host': host, 'mac': mac}}): {host: {'pin': pin, 'host': host, 'mac': mac}})
_LOGGER.error("Failed to save configuration file")
add_devices([BraviaTVDevice(host, mac, name, pin)]) add_devices([BraviaTVDevice(host, mac, name, pin)])

View File

@ -20,7 +20,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 = ['pychromecast==0.8.2'] REQUIREMENTS = ['pychromecast==1.0.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -6,7 +6,6 @@ https://home-assistant.io/components/media_player.gpmdp/
""" """
import logging import logging
import json import json
import os
import socket import socket
import time import time
@ -19,6 +18,7 @@ from homeassistant.components.media_player import (
from homeassistant.const import ( from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF, CONF_HOST, CONF_PORT, CONF_NAME) STATE_PLAYING, STATE_PAUSED, STATE_OFF, CONF_HOST, CONF_PORT, CONF_NAME)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['websocket-client==0.37.0'] REQUIREMENTS = ['websocket-client==0.37.0']
@ -86,8 +86,7 @@ def request_configuration(hass, config, url, add_devices_callback):
continue continue
setup_gpmdp(hass, config, code, setup_gpmdp(hass, config, code,
add_devices_callback) add_devices_callback)
_save_config(hass.config.path(GPMDP_CONFIG_FILE), save_json(hass.config.path(GPMDP_CONFIG_FILE), {"CODE": code})
{"CODE": code})
websocket.send(json.dumps({'namespace': 'connect', websocket.send(json.dumps({'namespace': 'connect',
'method': 'connect', 'method': 'connect',
'arguments': ['Home Assistant', code]})) 'arguments': ['Home Assistant', code]}))
@ -122,39 +121,9 @@ def setup_gpmdp(hass, config, code, add_devices):
add_devices([GPMDP(name, url, code)], True) add_devices([GPMDP(name, url, code)], True)
def _load_config(filename):
"""Load configuration."""
if not os.path.isfile(filename):
return {}
try:
with open(filename, 'r') as fdesc:
inp = fdesc.read()
# In case empty file
if not inp:
return {}
return json.loads(inp)
except (IOError, ValueError) as error:
_LOGGER.error("Reading config file %s failed: %s", filename, error)
return None
def _save_config(filename, config):
"""Save configuration."""
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(config, indent=4, sort_keys=True))
except (IOError, TypeError) as error:
_LOGGER.error("Saving configuration file failed: %s", error)
return False
return True
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the GPMDP platform.""" """Set up the GPMDP platform."""
codeconfig = _load_config(hass.config.path(GPMDP_CONFIG_FILE)) codeconfig = load_json(hass.config.path(GPMDP_CONFIG_FILE))
if codeconfig: if codeconfig:
code = codeconfig.get('CODE') code = codeconfig.get('CODE')
elif discovery_info is not None: elif discovery_info is not None:

View File

@ -121,13 +121,12 @@ def setup_plexserver(
_LOGGER.info("Discovery configuration done") _LOGGER.info("Discovery configuration done")
# Save config # Save config
if not save_json( save_json(
hass.config.path(PLEX_CONFIG_FILE), {host: { hass.config.path(PLEX_CONFIG_FILE), {host: {
'token': token, 'token': token,
'ssl': has_ssl, 'ssl': has_ssl,
'verify': verify_ssl, 'verify': verify_ssl,
}}): }})
_LOGGER.error("Failed to save configuration file")
_LOGGER.info('Connected to: %s://%s', http_prefix, host) _LOGGER.info('Connected to: %s://%s', http_prefix, host)

View File

@ -18,7 +18,7 @@ from homeassistant.util import Throttle
REQUIREMENTS = [ REQUIREMENTS = [
'https://github.com/jabesq/netatmo-api-python/archive/' 'https://github.com/jabesq/netatmo-api-python/archive/'
'v0.9.2.zip#lnetatmo==0.9.2'] 'v0.9.2.1.zip#lnetatmo==0.9.2.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -20,35 +20,39 @@ DEPENDENCIES = ['lametric']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_DISPLAY_TIME = "display_time" CONF_LIFETIME = "lifetime"
CONF_CYCLES = "cycles"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ICON, default="i555"): cv.string, vol.Optional(CONF_ICON, default="i555"): cv.string,
vol.Optional(CONF_DISPLAY_TIME, default=10): cv.positive_int, vol.Optional(CONF_LIFETIME, default=10): cv.positive_int,
vol.Optional(CONF_CYCLES, default=1): cv.positive_int,
}) })
# pylint: disable=unused-variable # pylint: disable=unused-variable
def get_service(hass, config, discovery_info=None): def get_service(hass, config, discovery_info=None):
"""Get the Slack notification service.""" """Get the LaMetric notification service."""
hlmn = hass.data.get(LAMETRIC_DOMAIN) hlmn = hass.data.get(LAMETRIC_DOMAIN)
return LaMetricNotificationService(hlmn, return LaMetricNotificationService(hlmn,
config[CONF_ICON], config[CONF_ICON],
config[CONF_DISPLAY_TIME] * 1000) config[CONF_LIFETIME] * 1000,
config[CONF_CYCLES])
class LaMetricNotificationService(BaseNotificationService): class LaMetricNotificationService(BaseNotificationService):
"""Implement the notification service for LaMetric.""" """Implement the notification service for LaMetric."""
def __init__(self, hasslametricmanager, icon, display_time): def __init__(self, hasslametricmanager, icon, lifetime, cycles):
"""Initialize the service.""" """Initialize the service."""
self.hasslametricmanager = hasslametricmanager self.hasslametricmanager = hasslametricmanager
self._icon = icon self._icon = icon
self._display_time = display_time self._lifetime = lifetime
self._cycles = cycles
# pylint: disable=broad-except # pylint: disable=broad-except
def send_message(self, message="", **kwargs): def send_message(self, message="", **kwargs):
"""Send a message to some LaMetric deviced.""" """Send a message to some LaMetric device."""
from lmnotify import SimpleFrame, Sound, Model from lmnotify import SimpleFrame, Sound, Model
from oauthlib.oauth2 import TokenExpiredError from oauthlib.oauth2 import TokenExpiredError
@ -56,9 +60,10 @@ class LaMetricNotificationService(BaseNotificationService):
data = kwargs.get(ATTR_DATA) data = kwargs.get(ATTR_DATA)
_LOGGER.debug("Targets/Data: %s/%s", targets, data) _LOGGER.debug("Targets/Data: %s/%s", targets, data)
icon = self._icon icon = self._icon
cycles = self._cycles
sound = None sound = None
# User-defined icon? # Additional data?
if data is not None: if data is not None:
if "icon" in data: if "icon" in data:
icon = data["icon"] icon = data["icon"]
@ -73,12 +78,12 @@ class LaMetricNotificationService(BaseNotificationService):
data["sound"]) data["sound"])
text_frame = SimpleFrame(icon, message) text_frame = SimpleFrame(icon, message)
_LOGGER.debug("Icon/Message/Duration: %s, %s, %d", _LOGGER.debug("Icon/Message/Cycles/Lifetime: %s, %s, %d, %d",
icon, message, self._display_time) icon, message, self._cycles, self._lifetime)
frames = [text_frame] frames = [text_frame]
model = Model(frames=frames, sound=sound) model = Model(frames=frames, cycles=cycles, sound=sound)
lmn = self.hasslametricmanager.manager lmn = self.hasslametricmanager.manager
try: try:
devices = lmn.get_devices() devices = lmn.get_devices()
@ -89,5 +94,5 @@ class LaMetricNotificationService(BaseNotificationService):
for dev in devices: for dev in devices:
if targets is None or dev["name"] in targets: if targets is None or dev["name"] in targets:
lmn.set_device(dev) lmn.set_device(dev)
lmn.send_notification(model, lifetime=self._display_time) lmn.send_notification(model, lifetime=self._lifetime)
_LOGGER.debug("Sent notification to LaMetric %s", dev["name"]) _LOGGER.debug("Sent notification to LaMetric %s", dev["name"])

View File

@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.matrix/ https://home-assistant.io/components/notify.matrix/
""" """
import logging import logging
import json
import os import os
from urllib.parse import urlparse from urllib.parse import urlparse
@ -15,6 +14,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA, from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA,
BaseNotificationService) BaseNotificationService)
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL
from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['matrix-client==0.0.6'] REQUIREMENTS = ['matrix-client==0.0.6']
@ -82,8 +82,7 @@ class MatrixNotificationService(BaseNotificationService):
return {} return {}
try: try:
with open(self.session_filepath) as handle: data = load_json(self.session_filepath)
data = json.load(handle)
auth_tokens = {} auth_tokens = {}
for mx_id, token in data.items(): for mx_id, token in data.items():
@ -101,16 +100,7 @@ class MatrixNotificationService(BaseNotificationService):
"""Store authentication token to session and persistent storage.""" """Store authentication token to session and persistent storage."""
self.auth_tokens[self.mx_id] = token self.auth_tokens[self.mx_id] = token
try: save_json(self.session_filepath, self.auth_tokens)
with open(self.session_filepath, 'w') as handle:
handle.write(json.dumps(self.auth_tokens))
# Not saving the tokens to disk should not stop the client, we can just
# login using the password every time.
except (OSError, IOError, PermissionError) as ex:
_LOGGER.warning(
"Storing authentication tokens to file '%s' failed: %s",
self.session_filepath, str(ex))
def login(self): def login(self):
"""Login to the matrix homeserver and return the client instance.""" """Login to the matrix homeserver and return the client instance."""

View File

@ -10,8 +10,8 @@ import mimetypes
import voluptuous as vol import voluptuous as vol
from homeassistant.components.notify import ( from homeassistant.components.notify import (
ATTR_DATA, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA,
PLATFORM_SCHEMA, BaseNotificationService) BaseNotificationService)
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -85,12 +85,12 @@ class PushBulletNotificationService(BaseNotificationService):
refreshed = False refreshed = False
if not targets: if not targets:
# Backward compatibility, notify all devices in own account # Backward compatibility, notify all devices in own account.
self._push_data(message, title, data, self.pushbullet) self._push_data(message, title, data, self.pushbullet)
_LOGGER.info("Sent notification to self") _LOGGER.info("Sent notification to self")
return return
# Main loop, process all targets specified # Main loop, process all targets specified.
for target in targets: for target in targets:
try: try:
ttype, tname = target.split('/', 1) ttype, tname = target.split('/', 1)
@ -98,15 +98,15 @@ class PushBulletNotificationService(BaseNotificationService):
_LOGGER.error("Invalid target syntax: %s", target) _LOGGER.error("Invalid target syntax: %s", target)
continue continue
# Target is email, send directly, don't use a target object # Target is email, send directly, don't use a target object.
# This also seems works to send to all devices in own account # This also seems works to send to all devices in own account.
if ttype == 'email': if ttype == 'email':
self._push_data(message, title, data, self.pushbullet, tname) self._push_data(message, title, data, self.pushbullet, tname)
_LOGGER.info("Sent notification to email %s", tname) _LOGGER.info("Sent notification to email %s", tname)
continue continue
# Refresh if name not found. While awaiting periodic refresh # Refresh if name not found. While awaiting periodic refresh
# solution in component, poor mans refresh ;) # solution in component, poor mans refresh.
if ttype not in self.pbtargets: if ttype not in self.pbtargets:
_LOGGER.error("Invalid target syntax: %s", target) _LOGGER.error("Invalid target syntax: %s", target)
continue continue
@ -128,6 +128,7 @@ class PushBulletNotificationService(BaseNotificationService):
continue continue
def _push_data(self, message, title, data, pusher, tname=None): def _push_data(self, message, title, data, pusher, tname=None):
"""Helper for creating the message content."""
from pushbullet import PushError from pushbullet import PushError
if data is None: if data is None:
data = {} data = {}
@ -142,17 +143,17 @@ class PushBulletNotificationService(BaseNotificationService):
pusher.push_link(title, url, body=message) pusher.push_link(title, url, body=message)
elif filepath: elif filepath:
if not self.hass.config.is_allowed_path(filepath): if not self.hass.config.is_allowed_path(filepath):
_LOGGER.error("Filepath is not valid or allowed.") _LOGGER.error("Filepath is not valid or allowed")
return return
with open(filepath, "rb") as fileh: with open(filepath, 'rb') as fileh:
filedata = self.pushbullet.upload_file(fileh, filepath) filedata = self.pushbullet.upload_file(fileh, filepath)
if filedata.get('file_type') == 'application/x-empty': if filedata.get('file_type') == 'application/x-empty':
_LOGGER.error("Can not send an empty file.") _LOGGER.error("Can not send an empty file")
return return
pusher.push_file(title=title, body=message, **filedata) pusher.push_file(title=title, body=message, **filedata)
elif file_url: elif file_url:
if not file_url.startswith('http'): if not file_url.startswith('http'):
_LOGGER.error("Url should start with http or https.") _LOGGER.error("URL should start with http or https")
return return
pusher.push_file(title=title, body=message, file_name=file_url, pusher.push_file(title=title, body=message, file_name=file_url,
file_url=file_url, file_url=file_url,

View File

@ -12,14 +12,14 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from requests.exceptions import HTTPError, ConnectTimeout from requests.exceptions import HTTPError, ConnectTimeout
REQUIREMENTS = ['ring_doorbell==0.1.7'] REQUIREMENTS = ['ring_doorbell==0.1.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = "Data provided by Ring.com" CONF_ATTRIBUTION = "Data provided by Ring.com"
NOTIFICATION_ID = 'ring_notification' NOTIFICATION_ID = 'ring_notification'
NOTIFICATION_TITLE = 'Ring Sensor Setup' NOTIFICATION_TITLE = 'Ring Setup'
DATA_RING = 'ring' DATA_RING = 'ring'
DOMAIN = 'ring' DOMAIN = 'ring'

View File

@ -8,9 +8,9 @@ import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
from homeassistant.components.amcrest import SENSORS from homeassistant.components.amcrest import DATA_AMCREST, SENSORS
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import STATE_UNKNOWN from homeassistant.const import CONF_NAME, CONF_SENSORS, STATE_UNKNOWN
DEPENDENCIES = ['amcrest'] DEPENDENCIES = ['amcrest']
@ -25,13 +25,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if discovery_info is None: if discovery_info is None:
return return
device = discovery_info['device'] device_name = discovery_info[CONF_NAME]
name = discovery_info['name'] sensors = discovery_info[CONF_SENSORS]
sensors = discovery_info['sensors'] amcrest = hass.data[DATA_AMCREST][device_name]
amcrest_sensors = [] amcrest_sensors = []
for sensor_type in sensors: for sensor_type in sensors:
amcrest_sensors.append(AmcrestSensor(name, device, sensor_type)) amcrest_sensors.append(
AmcrestSensor(amcrest.name, amcrest.device, sensor_type))
async_add_devices(amcrest_sensors, True) async_add_devices(amcrest_sensors, True)
return True return True

View File

@ -68,10 +68,15 @@ class CurrencylayerSensor(Entity):
self._base = base self._base = base
self._state = None self._state = None
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._quote
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return '{} {}'.format(self._base, self._quote) return self._base
@property @property
def icon(self): def icon(self):

View File

@ -4,17 +4,17 @@ Support for information about the German train system.
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/sensor.deutsche_bahn/ https://home-assistant.io/components/sensor.deutsche_bahn/
""" """
import logging
from datetime import timedelta from datetime import timedelta
import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['schiene==0.18'] REQUIREMENTS = ['schiene==0.19']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -6,16 +6,17 @@ https://home-assistant.io/components/sensor.fastdotcom/
""" """
import asyncio import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.util.dt as dt_util from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import (DOMAIN, PLATFORM_SCHEMA)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_change from homeassistant.helpers.event import track_time_change
from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.restore_state import async_get_last_state
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['fastdotcom==0.0.1'] REQUIREMENTS = ['fastdotcom==0.0.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.fitbit/ https://home-assistant.io/components/sensor.fitbit/
""" """
import os import os
import json
import logging import logging
import datetime import datetime
import time import time
@ -19,6 +18,8 @@ from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.icon import icon_for_battery_level
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['fitbit==0.3.0'] REQUIREMENTS = ['fitbit==0.3.0']
@ -147,31 +148,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
def config_from_file(filename, config=None):
"""Small configuration file management function."""
if config:
# We"re writing configuration
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(config))
except IOError as error:
_LOGGER.error("Saving config file failed: %s", error)
return False
return config
else:
# We"re reading config
if os.path.isfile(filename):
try:
with open(filename, 'r') as fdesc:
return json.loads(fdesc.read())
except IOError as error:
_LOGGER.error("Reading config file failed: %s", error)
# This won"t work yet
return False
else:
return {}
def request_app_setup(hass, config, add_devices, config_path, def request_app_setup(hass, config, add_devices, config_path,
discovery_info=None): discovery_info=None):
"""Assist user with configuring the Fitbit dev application.""" """Assist user with configuring the Fitbit dev application."""
@ -182,7 +158,7 @@ def request_app_setup(hass, config, add_devices, config_path,
"""Handle configuration updates.""" """Handle configuration updates."""
config_path = hass.config.path(FITBIT_CONFIG_FILE) config_path = hass.config.path(FITBIT_CONFIG_FILE)
if os.path.isfile(config_path): if os.path.isfile(config_path):
config_file = config_from_file(config_path) config_file = load_json(config_path)
if config_file == DEFAULT_CONFIG: if config_file == DEFAULT_CONFIG:
error_msg = ("You didn't correctly modify fitbit.conf", error_msg = ("You didn't correctly modify fitbit.conf",
" please try again") " please try again")
@ -242,13 +218,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Fitbit sensor.""" """Set up the Fitbit sensor."""
config_path = hass.config.path(FITBIT_CONFIG_FILE) config_path = hass.config.path(FITBIT_CONFIG_FILE)
if os.path.isfile(config_path): if os.path.isfile(config_path):
config_file = config_from_file(config_path) config_file = load_json(config_path)
if config_file == DEFAULT_CONFIG: if config_file == DEFAULT_CONFIG:
request_app_setup( request_app_setup(
hass, config, add_devices, config_path, discovery_info=None) hass, config, add_devices, config_path, discovery_info=None)
return False return False
else: else:
config_file = config_from_file(config_path, DEFAULT_CONFIG) config_file = save_json(config_path, DEFAULT_CONFIG)
request_app_setup( request_app_setup(
hass, config, add_devices, config_path, discovery_info=None) hass, config, add_devices, config_path, discovery_info=None)
return False return False
@ -384,9 +360,7 @@ class FitbitAuthCallbackView(HomeAssistantView):
ATTR_CLIENT_SECRET: self.oauth.client_secret, ATTR_CLIENT_SECRET: self.oauth.client_secret,
ATTR_LAST_SAVED_AT: int(time.time()) ATTR_LAST_SAVED_AT: int(time.time())
} }
if not config_from_file(hass.config.path(FITBIT_CONFIG_FILE), save_json(hass.config.path(FITBIT_CONFIG_FILE), config_contents)
config_contents):
_LOGGER.error("Failed to save config file")
hass.async_add_job(setup_platform, hass, self.config, self.add_devices) hass.async_add_job(setup_platform, hass, self.config, self.add_devices)
@ -513,5 +487,4 @@ class FitbitSensor(Entity):
ATTR_CLIENT_SECRET: self.client.client.client_secret, ATTR_CLIENT_SECRET: self.client.client.client_secret,
ATTR_LAST_SAVED_AT: int(time.time()) ATTR_LAST_SAVED_AT: int(time.time())
} }
if not config_from_file(self.config_path, config_contents): save_json(self.config_path, config_contents)
_LOGGER.error("Failed to save config file")

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