12
.coveragerc
@ -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
@ -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/*
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 226 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 13 KiB |
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
63
homeassistant/components/binary_sensor/hive.py
Normal 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)
|
@ -25,6 +25,7 @@ SENSOR_TYPES_CLASS = {
|
|||||||
'RemoteMotion': None,
|
'RemoteMotion': None,
|
||||||
'WeatherSensor': None,
|
'WeatherSensor': None,
|
||||||
'TiltSensor': None,
|
'TiltSensor': None,
|
||||||
|
'PresenceIP': 'motion',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 43 KiB |
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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)
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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():
|
||||||
|
@ -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))
|
||||||
|
@ -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."""
|
||||||
|
139
homeassistant/components/climate/hive.py
Normal 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)
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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:
|
||||||
|
@ -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']
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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'.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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
73
homeassistant/components/cover/tahoma.py
Normal 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')
|
134
homeassistant/components/device_tracker/unifi_direct.py
Normal 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 {}
|
@ -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'),
|
||||||
|
240
homeassistant/components/dominos.py
Normal 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')
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
After Width: | Height: | Size: 7.6 KiB |
@ -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}))
|
||||||
|
@ -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:
|
||||||
|
80
homeassistant/components/hive.py
Normal 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
|
@ -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']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
126
homeassistant/components/light/hive.py
Normal 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)
|
@ -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.
|
||||||
|
|
||||||
|

|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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'))
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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)])
|
||||||
|
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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"])
|
||||||
|
@ -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."""
|
||||||
|
@ -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,
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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")
|
|
||||||
|