mirror of
https://github.com/home-assistant/core.git
synced 2025-07-29 08:07:45 +00:00
commit
4239a2b844
14
.coveragerc
14
.coveragerc
@ -16,6 +16,9 @@ omit =
|
|||||||
homeassistant/components/bloomsky.py
|
homeassistant/components/bloomsky.py
|
||||||
homeassistant/components/*/bloomsky.py
|
homeassistant/components/*/bloomsky.py
|
||||||
|
|
||||||
|
homeassistant/components/digital_ocean.py
|
||||||
|
homeassistant/components/*/digital_ocean.py
|
||||||
|
|
||||||
homeassistant/components/dweet.py
|
homeassistant/components/dweet.py
|
||||||
homeassistant/components/*/dweet.py
|
homeassistant/components/*/dweet.py
|
||||||
|
|
||||||
@ -46,6 +49,9 @@ omit =
|
|||||||
homeassistant/components/qwikswitch.py
|
homeassistant/components/qwikswitch.py
|
||||||
homeassistant/components/*/qwikswitch.py
|
homeassistant/components/*/qwikswitch.py
|
||||||
|
|
||||||
|
homeassistant/components/rfxtrx.py
|
||||||
|
homeassistant/components/*/rfxtrx.py
|
||||||
|
|
||||||
homeassistant/components/rpi_gpio.py
|
homeassistant/components/rpi_gpio.py
|
||||||
homeassistant/components/*/rpi_gpio.py
|
homeassistant/components/*/rpi_gpio.py
|
||||||
|
|
||||||
@ -77,7 +83,7 @@ omit =
|
|||||||
homeassistant/components/zigbee.py
|
homeassistant/components/zigbee.py
|
||||||
homeassistant/components/*/zigbee.py
|
homeassistant/components/*/zigbee.py
|
||||||
|
|
||||||
homeassistant/components/zwave.py
|
homeassistant/components/zwave/*
|
||||||
homeassistant/components/*/zwave.py
|
homeassistant/components/*/zwave.py
|
||||||
|
|
||||||
homeassistant/components/enocean.py
|
homeassistant/components/enocean.py
|
||||||
@ -135,6 +141,7 @@ omit =
|
|||||||
homeassistant/components/device_tracker/tomato.py
|
homeassistant/components/device_tracker/tomato.py
|
||||||
homeassistant/components/device_tracker/tplink.py
|
homeassistant/components/device_tracker/tplink.py
|
||||||
homeassistant/components/device_tracker/ubus.py
|
homeassistant/components/device_tracker/ubus.py
|
||||||
|
homeassistant/components/device_tracker/volvooncall.py
|
||||||
homeassistant/components/discovery.py
|
homeassistant/components/discovery.py
|
||||||
homeassistant/components/downloader.py
|
homeassistant/components/downloader.py
|
||||||
homeassistant/components/fan/mqtt.py
|
homeassistant/components/fan/mqtt.py
|
||||||
@ -213,6 +220,7 @@ omit =
|
|||||||
homeassistant/components/sensor/bom.py
|
homeassistant/components/sensor/bom.py
|
||||||
homeassistant/components/sensor/coinmarketcap.py
|
homeassistant/components/sensor/coinmarketcap.py
|
||||||
homeassistant/components/sensor/cpuspeed.py
|
homeassistant/components/sensor/cpuspeed.py
|
||||||
|
homeassistant/components/sensor/darksky.py
|
||||||
homeassistant/components/sensor/deutsche_bahn.py
|
homeassistant/components/sensor/deutsche_bahn.py
|
||||||
homeassistant/components/sensor/dht.py
|
homeassistant/components/sensor/dht.py
|
||||||
homeassistant/components/sensor/dte_energy_bridge.py
|
homeassistant/components/sensor/dte_energy_bridge.py
|
||||||
@ -222,7 +230,6 @@ omit =
|
|||||||
homeassistant/components/sensor/fastdotcom.py
|
homeassistant/components/sensor/fastdotcom.py
|
||||||
homeassistant/components/sensor/fitbit.py
|
homeassistant/components/sensor/fitbit.py
|
||||||
homeassistant/components/sensor/fixer.py
|
homeassistant/components/sensor/fixer.py
|
||||||
homeassistant/components/sensor/forecast.py
|
|
||||||
homeassistant/components/sensor/fritzbox_callmonitor.py
|
homeassistant/components/sensor/fritzbox_callmonitor.py
|
||||||
homeassistant/components/sensor/glances.py
|
homeassistant/components/sensor/glances.py
|
||||||
homeassistant/components/sensor/google_travel_time.py
|
homeassistant/components/sensor/google_travel_time.py
|
||||||
@ -255,17 +262,20 @@ omit =
|
|||||||
homeassistant/components/sensor/swiss_hydrological_data.py
|
homeassistant/components/sensor/swiss_hydrological_data.py
|
||||||
homeassistant/components/sensor/swiss_public_transport.py
|
homeassistant/components/sensor/swiss_public_transport.py
|
||||||
homeassistant/components/sensor/systemmonitor.py
|
homeassistant/components/sensor/systemmonitor.py
|
||||||
|
homeassistant/components/sensor/ted5000.py
|
||||||
homeassistant/components/sensor/temper.py
|
homeassistant/components/sensor/temper.py
|
||||||
homeassistant/components/sensor/time_date.py
|
homeassistant/components/sensor/time_date.py
|
||||||
homeassistant/components/sensor/torque.py
|
homeassistant/components/sensor/torque.py
|
||||||
homeassistant/components/sensor/transmission.py
|
homeassistant/components/sensor/transmission.py
|
||||||
homeassistant/components/sensor/twitch.py
|
homeassistant/components/sensor/twitch.py
|
||||||
homeassistant/components/sensor/uber.py
|
homeassistant/components/sensor/uber.py
|
||||||
|
homeassistant/components/sensor/vasttrafik.py
|
||||||
homeassistant/components/sensor/worldclock.py
|
homeassistant/components/sensor/worldclock.py
|
||||||
homeassistant/components/sensor/xbox_live.py
|
homeassistant/components/sensor/xbox_live.py
|
||||||
homeassistant/components/sensor/yahoo_finance.py
|
homeassistant/components/sensor/yahoo_finance.py
|
||||||
homeassistant/components/sensor/yweather.py
|
homeassistant/components/sensor/yweather.py
|
||||||
homeassistant/components/switch/acer_projector.py
|
homeassistant/components/switch/acer_projector.py
|
||||||
|
homeassistant/components/switch/anel_pwrctrl.py
|
||||||
homeassistant/components/switch/arest.py
|
homeassistant/components/switch/arest.py
|
||||||
homeassistant/components/switch/dlink.py
|
homeassistant/components/switch/dlink.py
|
||||||
homeassistant/components/switch/edimax.py
|
homeassistant/components/switch/edimax.py
|
||||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -15,7 +15,7 @@
|
|||||||
If user exposed functionality or configuration variables are added/changed:
|
If user exposed functionality or configuration variables are added/changed:
|
||||||
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
|
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
|
||||||
|
|
||||||
If code communicates with devices, web services, or a:
|
If the code communicates with devices, web services, or third-party tools:
|
||||||
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
|
||||||
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
|
||||||
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
|
||||||
|
@ -340,8 +340,8 @@ latex_elements = {
|
|||||||
# (source start file, target name, title,
|
# (source start file, target name, title,
|
||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
(master_doc, 'Home-Assistant.tex', 'Home-Assistant Documentation',
|
(master_doc, 'home-assistant.tex', 'Home Assistant Documentation',
|
||||||
'Home-Assistant Team', 'manual'),
|
'Home Assistant Team', 'manual'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
@ -382,7 +382,7 @@ latex_documents = [
|
|||||||
# One entry per manual page. List of tuples
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [
|
||||||
(master_doc, 'home-assistant', 'Home-Assistant Documentation',
|
(master_doc, 'home-assistant', 'Home Assistant Documentation',
|
||||||
[author], 1)
|
[author], 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -397,8 +397,8 @@ man_pages = [
|
|||||||
# (source start file, target name, title, author,
|
# (source start file, target name, title, author,
|
||||||
# dir menu entry, description, category)
|
# dir menu entry, description, category)
|
||||||
texinfo_documents = [
|
texinfo_documents = [
|
||||||
(master_doc, 'Home-Assistant', 'Home-Assistant Documentation',
|
(master_doc, 'Home-Assistant', 'Home Assistant Documentation',
|
||||||
author, 'Home-Assistant', 'One line description of project.',
|
author, 'Home Assistant', 'Open-source home automation platform.',
|
||||||
'Miscellaneous'),
|
'Miscellaneous'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ Allow to setup simple automation rules via the config file.
|
|||||||
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/automation/
|
https://home-assistant.io/components/automation/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@ -23,12 +24,15 @@ from homeassistant.helpers.entity_component import EntityComponent
|
|||||||
from homeassistant.loader import get_platform
|
from homeassistant.loader import get_platform
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.util.async import run_coroutine_threadsafe
|
||||||
|
|
||||||
DOMAIN = 'automation'
|
DOMAIN = 'automation'
|
||||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
|
||||||
DEPENDENCIES = ['group']
|
DEPENDENCIES = ['group']
|
||||||
|
|
||||||
|
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
|
||||||
|
|
||||||
CONF_ALIAS = 'alias'
|
CONF_ALIAS = 'alias'
|
||||||
CONF_HIDE_ENTITY = 'hide_entity'
|
CONF_HIDE_ENTITY = 'hide_entity'
|
||||||
|
|
||||||
@ -36,6 +40,7 @@ CONF_CONDITION = 'condition'
|
|||||||
CONF_ACTION = 'action'
|
CONF_ACTION = 'action'
|
||||||
CONF_TRIGGER = 'trigger'
|
CONF_TRIGGER = 'trigger'
|
||||||
CONF_CONDITION_TYPE = 'condition_type'
|
CONF_CONDITION_TYPE = 'condition_type'
|
||||||
|
CONF_INITIAL_STATE = 'initial_state'
|
||||||
|
|
||||||
CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
|
CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
|
||||||
CONDITION_TYPE_AND = 'and'
|
CONDITION_TYPE_AND = 'and'
|
||||||
@ -43,9 +48,7 @@ CONDITION_TYPE_OR = 'or'
|
|||||||
|
|
||||||
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
||||||
DEFAULT_HIDE_ENTITY = False
|
DEFAULT_HIDE_ENTITY = False
|
||||||
|
DEFAULT_INITIAL_STATE = True
|
||||||
METHOD_TRIGGER = 'trigger'
|
|
||||||
METHOD_IF_ACTION = 'if_action'
|
|
||||||
|
|
||||||
ATTR_LAST_TRIGGERED = 'last_triggered'
|
ATTR_LAST_TRIGGERED = 'last_triggered'
|
||||||
ATTR_VARIABLES = 'variables'
|
ATTR_VARIABLES = 'variables'
|
||||||
@ -55,21 +58,14 @@ SERVICE_RELOAD = 'reload'
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _platform_validator(method, schema):
|
def _platform_validator(config):
|
||||||
"""Generate platform validator for different steps."""
|
"""Validate it is a valid platform."""
|
||||||
def validator(config):
|
platform = get_platform(DOMAIN, config[CONF_PLATFORM])
|
||||||
"""Validate it is a valid platform."""
|
|
||||||
platform = get_platform(DOMAIN, config[CONF_PLATFORM])
|
|
||||||
|
|
||||||
if not hasattr(platform, method):
|
if not hasattr(platform, 'TRIGGER_SCHEMA'):
|
||||||
raise vol.Invalid('invalid method platform')
|
return config
|
||||||
|
|
||||||
if not hasattr(platform, schema):
|
return getattr(platform, 'TRIGGER_SCHEMA')(config)
|
||||||
return config
|
|
||||||
|
|
||||||
return getattr(platform, schema)(config)
|
|
||||||
|
|
||||||
return validator
|
|
||||||
|
|
||||||
_TRIGGER_SCHEMA = vol.All(
|
_TRIGGER_SCHEMA = vol.All(
|
||||||
cv.ensure_list,
|
cv.ensure_list,
|
||||||
@ -78,33 +74,19 @@ _TRIGGER_SCHEMA = vol.All(
|
|||||||
vol.Schema({
|
vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN)
|
vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN)
|
||||||
}, extra=vol.ALLOW_EXTRA),
|
}, extra=vol.ALLOW_EXTRA),
|
||||||
_platform_validator(METHOD_TRIGGER, 'TRIGGER_SCHEMA')
|
_platform_validator
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
_CONDITION_SCHEMA = vol.Any(
|
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
|
||||||
CONDITION_USE_TRIGGER_VALUES,
|
|
||||||
vol.All(
|
|
||||||
cv.ensure_list,
|
|
||||||
[
|
|
||||||
vol.All(
|
|
||||||
vol.Schema({
|
|
||||||
CONF_PLATFORM: str,
|
|
||||||
CONF_CONDITION: str,
|
|
||||||
}, extra=vol.ALLOW_EXTRA),
|
|
||||||
cv.has_at_least_one_key(CONF_PLATFORM, CONF_CONDITION),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.Schema({
|
PLATFORM_SCHEMA = vol.Schema({
|
||||||
CONF_ALIAS: cv.string,
|
CONF_ALIAS: cv.string,
|
||||||
|
vol.Optional(CONF_INITIAL_STATE,
|
||||||
|
default=DEFAULT_INITIAL_STATE): cv.boolean,
|
||||||
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
|
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
|
||||||
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
|
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
|
||||||
vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE):
|
|
||||||
vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)),
|
|
||||||
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
|
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
|
||||||
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||||
})
|
})
|
||||||
@ -163,9 +145,11 @@ def reload(hass):
|
|||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup the automation."""
|
"""Setup the automation."""
|
||||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||||
|
group_name=GROUP_NAME_ALL_AUTOMATIONS)
|
||||||
|
|
||||||
success = _process_config(hass, config, component)
|
success = run_coroutine_threadsafe(
|
||||||
|
_async_process_config(hass, config, component), hass.loop).result()
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
return False
|
return False
|
||||||
@ -173,22 +157,37 @@ def setup(hass, config):
|
|||||||
descriptions = conf_util.load_yaml_config_file(
|
descriptions = conf_util.load_yaml_config_file(
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def trigger_service_handler(service_call):
|
def trigger_service_handler(service_call):
|
||||||
"""Handle automation triggers."""
|
"""Handle automation triggers."""
|
||||||
for entity in component.extract_from_service(service_call):
|
for entity in component.extract_from_service(service_call):
|
||||||
entity.trigger(service_call.data.get(ATTR_VARIABLES))
|
hass.loop.create_task(entity.async_trigger(
|
||||||
|
service_call.data.get(ATTR_VARIABLES), True))
|
||||||
|
|
||||||
def service_handler(service_call):
|
@asyncio.coroutine
|
||||||
"""Handle automation service calls."""
|
def turn_onoff_service_handler(service_call):
|
||||||
|
"""Handle automation turn on/off service calls."""
|
||||||
|
method = 'async_{}'.format(service_call.service)
|
||||||
for entity in component.extract_from_service(service_call):
|
for entity in component.extract_from_service(service_call):
|
||||||
getattr(entity, service_call.service)()
|
hass.loop.create_task(getattr(entity, method)())
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def toggle_service_handler(service_call):
|
||||||
|
"""Handle automation toggle service calls."""
|
||||||
|
for entity in component.extract_from_service(service_call):
|
||||||
|
if entity.is_on:
|
||||||
|
hass.loop.create_task(entity.async_turn_off())
|
||||||
|
else:
|
||||||
|
hass.loop.create_task(entity.async_turn_on())
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def reload_service_handler(service_call):
|
def reload_service_handler(service_call):
|
||||||
"""Remove all automations and load new ones from config."""
|
"""Remove all automations and load new ones from config."""
|
||||||
conf = component.prepare_reload()
|
conf = yield from hass.loop.run_in_executor(
|
||||||
|
None, component.prepare_reload)
|
||||||
if conf is None:
|
if conf is None:
|
||||||
return
|
return
|
||||||
_process_config(hass, conf, component)
|
hass.loop.create_task(_async_process_config(hass, conf, component))
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
||||||
descriptions.get(SERVICE_TRIGGER),
|
descriptions.get(SERVICE_TRIGGER),
|
||||||
@ -198,8 +197,12 @@ def setup(hass, config):
|
|||||||
descriptions.get(SERVICE_RELOAD),
|
descriptions.get(SERVICE_RELOAD),
|
||||||
schema=RELOAD_SERVICE_SCHEMA)
|
schema=RELOAD_SERVICE_SCHEMA)
|
||||||
|
|
||||||
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE):
|
hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
|
||||||
hass.services.register(DOMAIN, service, service_handler,
|
descriptions.get(SERVICE_TOGGLE),
|
||||||
|
schema=SERVICE_SCHEMA)
|
||||||
|
|
||||||
|
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
|
||||||
|
hass.services.register(DOMAIN, service, turn_onoff_service_handler,
|
||||||
descriptions.get(service),
|
descriptions.get(service),
|
||||||
schema=SERVICE_SCHEMA)
|
schema=SERVICE_SCHEMA)
|
||||||
|
|
||||||
@ -209,15 +212,17 @@ def setup(hass, config):
|
|||||||
class AutomationEntity(ToggleEntity):
|
class AutomationEntity(ToggleEntity):
|
||||||
"""Entity to show status of entity."""
|
"""Entity to show status of entity."""
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||||
def __init__(self, name, attach_triggers, cond_func, action, hidden):
|
def __init__(self, name, async_attach_triggers, cond_func, async_action,
|
||||||
|
hidden):
|
||||||
"""Initialize an automation entity."""
|
"""Initialize an automation entity."""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._attach_triggers = attach_triggers
|
self._async_attach_triggers = async_attach_triggers
|
||||||
self._detach_triggers = attach_triggers(self.trigger)
|
self._async_detach_triggers = None
|
||||||
self._cond_func = cond_func
|
self._cond_func = cond_func
|
||||||
self._action = action
|
self._async_action = async_action
|
||||||
self._enabled = True
|
self._enabled = False
|
||||||
self._last_triggered = None
|
self._last_triggered = None
|
||||||
self._hidden = hidden
|
self._hidden = hidden
|
||||||
|
|
||||||
@ -248,41 +253,65 @@ class AutomationEntity(ToggleEntity):
|
|||||||
"""Return True if entity is on."""
|
"""Return True if entity is on."""
|
||||||
return self._enabled
|
return self._enabled
|
||||||
|
|
||||||
def turn_on(self, **kwargs) -> None:
|
@asyncio.coroutine
|
||||||
"""Turn the entity on."""
|
def async_turn_on(self, **kwargs) -> None:
|
||||||
|
"""Turn the entity on and update the state."""
|
||||||
if self._enabled:
|
if self._enabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._detach_triggers = self._attach_triggers(self.trigger)
|
yield from self.async_enable()
|
||||||
self._enabled = True
|
self.hass.loop.create_task(self.async_update_ha_state())
|
||||||
self.update_ha_state()
|
|
||||||
|
|
||||||
def turn_off(self, **kwargs) -> None:
|
@asyncio.coroutine
|
||||||
|
def async_turn_off(self, **kwargs) -> None:
|
||||||
"""Turn the entity off."""
|
"""Turn the entity off."""
|
||||||
if not self._enabled:
|
if not self._enabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._detach_triggers()
|
self._async_detach_triggers()
|
||||||
self._detach_triggers = None
|
self._async_detach_triggers = None
|
||||||
self._enabled = False
|
self._enabled = False
|
||||||
self.update_ha_state()
|
self.hass.loop.create_task(self.async_update_ha_state())
|
||||||
|
|
||||||
def trigger(self, variables):
|
@asyncio.coroutine
|
||||||
"""Trigger automation."""
|
def async_trigger(self, variables, skip_condition=False):
|
||||||
if self._cond_func(variables):
|
"""Trigger automation.
|
||||||
self._action(variables)
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
|
if skip_condition or self._cond_func(variables):
|
||||||
|
yield from self._async_action(variables)
|
||||||
self._last_triggered = utcnow()
|
self._last_triggered = utcnow()
|
||||||
self.update_ha_state()
|
self.hass.loop.create_task(self.async_update_ha_state())
|
||||||
|
|
||||||
def remove(self):
|
def remove(self):
|
||||||
"""Remove automation from HASS."""
|
"""Remove automation from HASS."""
|
||||||
self.turn_off()
|
run_coroutine_threadsafe(self.async_turn_off(),
|
||||||
|
self.hass.loop).result()
|
||||||
super().remove()
|
super().remove()
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_enable(self):
|
||||||
|
"""Enable this automation entity.
|
||||||
|
|
||||||
def _process_config(hass, config, component):
|
This method is a coroutine.
|
||||||
"""Process config and add automations."""
|
"""
|
||||||
success = False
|
if self._enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._async_detach_triggers = yield from self._async_attach_triggers(
|
||||||
|
self.async_trigger)
|
||||||
|
self._enabled = True
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _async_process_config(hass, config, component):
|
||||||
|
"""Process config and add automations.
|
||||||
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
|
entities = []
|
||||||
|
tasks = []
|
||||||
|
|
||||||
for config_key in extract_domain_configs(config, DOMAIN):
|
for config_key in extract_domain_configs(config, DOMAIN):
|
||||||
conf = config[config_key]
|
conf = config[config_key]
|
||||||
@ -293,10 +322,11 @@ def _process_config(hass, config, component):
|
|||||||
|
|
||||||
hidden = config_block[CONF_HIDE_ENTITY]
|
hidden = config_block[CONF_HIDE_ENTITY]
|
||||||
|
|
||||||
action = _get_action(hass, config_block.get(CONF_ACTION, {}), name)
|
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
|
||||||
|
name)
|
||||||
|
|
||||||
if CONF_CONDITION in config_block:
|
if CONF_CONDITION in config_block:
|
||||||
cond_func = _process_if(hass, config, config_block)
|
cond_func = _async_process_if(hass, config, config_block)
|
||||||
|
|
||||||
if cond_func is None:
|
if cond_func is None:
|
||||||
continue
|
continue
|
||||||
@ -305,101 +335,78 @@ def _process_config(hass, config, component):
|
|||||||
"""Condition will always pass."""
|
"""Condition will always pass."""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
attach_triggers = partial(_process_trigger, hass, config,
|
async_attach_triggers = partial(
|
||||||
config_block.get(CONF_TRIGGER, []), name)
|
_async_process_trigger, hass, config,
|
||||||
entity = AutomationEntity(name, attach_triggers, cond_func, action,
|
config_block.get(CONF_TRIGGER, []), name)
|
||||||
hidden)
|
entity = AutomationEntity(name, async_attach_triggers, cond_func,
|
||||||
component.add_entities((entity,))
|
action, hidden)
|
||||||
success = True
|
if config_block[CONF_INITIAL_STATE]:
|
||||||
|
tasks.append(hass.loop.create_task(entity.async_enable()))
|
||||||
|
entities.append(entity)
|
||||||
|
|
||||||
return success
|
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||||
|
yield from hass.loop.run_in_executor(
|
||||||
|
None, component.add_entities, entities)
|
||||||
|
|
||||||
|
return len(entities) > 0
|
||||||
|
|
||||||
|
|
||||||
def _get_action(hass, config, name):
|
def _async_get_action(hass, config, name):
|
||||||
"""Return an action based on a configuration."""
|
"""Return an action based on a configuration."""
|
||||||
script_obj = script.Script(hass, config, name)
|
script_obj = script.Script(hass, config, name)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
def action(variables=None):
|
def action(variables=None):
|
||||||
"""Action to be executed."""
|
"""Action to be executed."""
|
||||||
_LOGGER.info('Executing %s', name)
|
_LOGGER.info('Executing %s', name)
|
||||||
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
logbook.async_log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||||
script_obj.run(variables)
|
hass.loop.create_task(script_obj.async_run(variables))
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
||||||
|
|
||||||
def _process_if(hass, config, p_config):
|
def _async_process_if(hass, config, p_config):
|
||||||
"""Process if checks."""
|
"""Process if checks."""
|
||||||
cond_type = p_config.get(CONF_CONDITION_TYPE,
|
|
||||||
DEFAULT_CONDITION_TYPE).lower()
|
|
||||||
|
|
||||||
# Deprecated since 0.19 - 5/5/2016
|
|
||||||
if cond_type != DEFAULT_CONDITION_TYPE:
|
|
||||||
_LOGGER.warning('Using condition_type: "or" is deprecated. Please use '
|
|
||||||
'"condition: or" instead.')
|
|
||||||
|
|
||||||
if_configs = p_config.get(CONF_CONDITION)
|
if_configs = p_config.get(CONF_CONDITION)
|
||||||
use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES
|
|
||||||
|
|
||||||
if use_trigger:
|
|
||||||
if_configs = p_config[CONF_TRIGGER]
|
|
||||||
|
|
||||||
checks = []
|
checks = []
|
||||||
for if_config in if_configs:
|
for if_config in if_configs:
|
||||||
# Deprecated except for used by use_trigger_values
|
|
||||||
# since 0.19 - 5/5/2016
|
|
||||||
if CONF_PLATFORM in if_config:
|
|
||||||
if not use_trigger:
|
|
||||||
_LOGGER.warning("Please switch your condition configuration "
|
|
||||||
"to use 'condition' instead of 'platform'.")
|
|
||||||
if_config = dict(if_config)
|
|
||||||
if_config[CONF_CONDITION] = if_config.pop(CONF_PLATFORM)
|
|
||||||
|
|
||||||
# To support use_trigger_values with state trigger accepting
|
|
||||||
# multiple entity_ids to monitor.
|
|
||||||
if_entity_id = if_config.get(ATTR_ENTITY_ID)
|
|
||||||
if isinstance(if_entity_id, list) and len(if_entity_id) == 1:
|
|
||||||
if_config[ATTR_ENTITY_ID] = if_entity_id[0]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
checks.append(condition.from_config(if_config))
|
checks.append(condition.async_from_config(if_config, False))
|
||||||
except HomeAssistantError as ex:
|
except HomeAssistantError as ex:
|
||||||
# Invalid conditions are allowed if we base it on trigger
|
_LOGGER.warning('Invalid condition: %s', ex)
|
||||||
if use_trigger:
|
return None
|
||||||
_LOGGER.warning('Ignoring invalid condition: %s', ex)
|
|
||||||
else:
|
|
||||||
_LOGGER.warning('Invalid condition: %s', ex)
|
|
||||||
return None
|
|
||||||
|
|
||||||
if cond_type == CONDITION_TYPE_AND:
|
def if_action(variables=None):
|
||||||
def if_action(variables=None):
|
"""AND all conditions."""
|
||||||
"""AND all conditions."""
|
return all(check(hass, variables) for check in checks)
|
||||||
return all(check(hass, variables) for check in checks)
|
|
||||||
else:
|
|
||||||
def if_action(variables=None):
|
|
||||||
"""OR all conditions."""
|
|
||||||
return any(check(hass, variables) for check in checks)
|
|
||||||
|
|
||||||
return if_action
|
return if_action
|
||||||
|
|
||||||
|
|
||||||
def _process_trigger(hass, config, trigger_configs, name, action):
|
@asyncio.coroutine
|
||||||
"""Setup the triggers."""
|
def _async_process_trigger(hass, config, trigger_configs, name, action):
|
||||||
|
"""Setup the triggers.
|
||||||
|
|
||||||
|
This method is a coroutine.
|
||||||
|
"""
|
||||||
removes = []
|
removes = []
|
||||||
|
|
||||||
for conf in trigger_configs:
|
for conf in trigger_configs:
|
||||||
platform = _resolve_platform(METHOD_TRIGGER, hass, config,
|
platform = yield from hass.loop.run_in_executor(
|
||||||
conf.get(CONF_PLATFORM))
|
None, prepare_setup_platform, hass, config, DOMAIN,
|
||||||
if platform is None:
|
conf.get(CONF_PLATFORM))
|
||||||
continue
|
|
||||||
|
|
||||||
remove = platform.trigger(hass, conf, action)
|
if platform is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
remove = platform.async_trigger(hass, conf, action)
|
||||||
|
|
||||||
if not remove:
|
if not remove:
|
||||||
_LOGGER.error("Error setting up rule %s", name)
|
_LOGGER.error("Error setting up trigger %s", name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
_LOGGER.info("Initialized rule %s", name)
|
_LOGGER.info("Initialized trigger %s", name)
|
||||||
removes.append(remove)
|
removes.append(remove)
|
||||||
|
|
||||||
if not removes:
|
if not removes:
|
||||||
@ -411,17 +418,3 @@ def _process_trigger(hass, config, trigger_configs, name, action):
|
|||||||
remove()
|
remove()
|
||||||
|
|
||||||
return remove_triggers
|
return remove_triggers
|
||||||
|
|
||||||
|
|
||||||
def _resolve_platform(method, hass, config, platform):
|
|
||||||
"""Find the automation platform."""
|
|
||||||
if platform is None:
|
|
||||||
return None
|
|
||||||
platform = prepare_setup_platform(hass, config, DOMAIN, platform)
|
|
||||||
|
|
||||||
if platform is None or not hasattr(platform, method):
|
|
||||||
_LOGGER.error("Unknown automation platform specified for %s: %s",
|
|
||||||
method, platform)
|
|
||||||
return None
|
|
||||||
|
|
||||||
return platform
|
|
||||||
|
@ -4,11 +4,11 @@ Offer event listening automation rules.
|
|||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#event-trigger
|
at https://home-assistant.io/components/automation/#event-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import CONF_PLATFORM
|
from homeassistant.const import CONF_PLATFORM
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
@ -24,21 +24,21 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def trigger(hass, config, action):
|
def async_trigger(hass, config, action):
|
||||||
"""Listen for events based on configuration."""
|
"""Listen for events based on configuration."""
|
||||||
event_type = config.get(CONF_EVENT_TYPE)
|
event_type = config.get(CONF_EVENT_TYPE)
|
||||||
event_data = config.get(CONF_EVENT_DATA)
|
event_data = config.get(CONF_EVENT_DATA)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@callback
|
||||||
def handle_event(event):
|
def handle_event(event):
|
||||||
"""Listen for events and calls the action when data matches."""
|
"""Listen for events and calls the action when data matches."""
|
||||||
if not event_data or all(val == event.data.get(key) for key, val
|
if not event_data or all(val == event.data.get(key) for key, val
|
||||||
in event_data.items()):
|
in event_data.items()):
|
||||||
hass.async_add_job(action, {
|
hass.async_run_job(action, {
|
||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'event',
|
'platform': 'event',
|
||||||
'event': event,
|
'event': event,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return hass.bus.listen(event_type, handle_event)
|
return hass.bus.async_listen(event_type, handle_event)
|
||||||
|
@ -6,6 +6,7 @@ at https://home-assistant.io/components/automation/#mqtt-trigger
|
|||||||
"""
|
"""
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
|
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -21,15 +22,16 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def trigger(hass, config, action):
|
def async_trigger(hass, config, action):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
topic = config.get(CONF_TOPIC)
|
topic = config.get(CONF_TOPIC)
|
||||||
payload = config.get(CONF_PAYLOAD)
|
payload = config.get(CONF_PAYLOAD)
|
||||||
|
|
||||||
|
@callback
|
||||||
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
||||||
"""Listen for MQTT messages."""
|
"""Listen for MQTT messages."""
|
||||||
if payload is None or payload == msg_payload:
|
if payload is None or payload == msg_payload:
|
||||||
action({
|
hass.async_run_job(action, {
|
||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'mqtt',
|
'platform': 'mqtt',
|
||||||
'topic': msg_topic,
|
'topic': msg_topic,
|
||||||
@ -38,4 +40,4 @@ def trigger(hass, config, action):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return mqtt.subscribe(hass, topic, mqtt_automation_listener)
|
return mqtt.async_subscribe(hass, topic, mqtt_automation_listener)
|
||||||
|
@ -8,10 +8,11 @@ import logging
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
|
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
|
||||||
CONF_BELOW, CONF_ABOVE)
|
CONF_BELOW, CONF_ABOVE)
|
||||||
from homeassistant.helpers.event import track_state_change
|
from homeassistant.helpers.event import async_track_state_change
|
||||||
from homeassistant.helpers import condition, config_validation as cv
|
from homeassistant.helpers import condition, config_validation as cv
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||||
@ -25,7 +26,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def trigger(hass, config, action):
|
def async_trigger(hass, config, action):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
entity_id = config.get(CONF_ENTITY_ID)
|
entity_id = config.get(CONF_ENTITY_ID)
|
||||||
below = config.get(CONF_BELOW)
|
below = config.get(CONF_BELOW)
|
||||||
@ -34,7 +35,7 @@ def trigger(hass, config, action):
|
|||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
@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."""
|
||||||
if to_s is None:
|
if to_s is None:
|
||||||
@ -50,19 +51,19 @@ def trigger(hass, config, action):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# If new one doesn't match, nothing to do
|
# If new one doesn't match, nothing to do
|
||||||
if not condition.numeric_state(
|
if not condition.async_numeric_state(
|
||||||
hass, to_s, below, above, value_template, variables):
|
hass, to_s, below, above, value_template, variables):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Only match if old didn't exist or existed but didn't match
|
# Only match if old didn't exist or existed but didn't match
|
||||||
# Written as: skip if old one did exist and matched
|
# Written as: skip if old one did exist and matched
|
||||||
if from_s is not None and condition.numeric_state(
|
if from_s is not None and condition.async_numeric_state(
|
||||||
hass, from_s, below, above, value_template, variables):
|
hass, from_s, below, above, value_template, variables):
|
||||||
return
|
return
|
||||||
|
|
||||||
variables['trigger']['from_state'] = from_s
|
variables['trigger']['from_state'] = from_s
|
||||||
variables['trigger']['to_state'] = to_s
|
variables['trigger']['to_state'] = to_s
|
||||||
|
|
||||||
action(variables)
|
hass.async_run_job(action, variables)
|
||||||
|
|
||||||
return track_state_change(hass, entity_id, state_automation_listener)
|
return async_track_state_change(hass, entity_id, state_automation_listener)
|
||||||
|
@ -6,9 +6,11 @@ at https://home-assistant.io/components/automation/#state-trigger
|
|||||||
"""
|
"""
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM
|
from homeassistant.const import MATCH_ALL, CONF_PLATFORM
|
||||||
from homeassistant.helpers.event import track_state_change, track_point_in_time
|
from homeassistant.helpers.event import (
|
||||||
|
async_track_state_change, async_track_point_in_utc_time)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
CONF_ENTITY_ID = "entity_id"
|
CONF_ENTITY_ID = "entity_id"
|
||||||
@ -32,22 +34,23 @@ TRIGGER_SCHEMA = vol.All(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def trigger(hass, config, action):
|
def async_trigger(hass, config, action):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
entity_id = config.get(CONF_ENTITY_ID)
|
entity_id = config.get(CONF_ENTITY_ID)
|
||||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||||
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
|
to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL
|
||||||
time_delta = config.get(CONF_FOR)
|
time_delta = config.get(CONF_FOR)
|
||||||
remove_state_for_cancel = None
|
async_remove_state_for_cancel = None
|
||||||
remove_state_for_listener = None
|
async_remove_state_for_listener = None
|
||||||
|
|
||||||
|
@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 remove_state_for_cancel, remove_state_for_listener
|
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
|
||||||
|
|
||||||
def call_action():
|
def call_action():
|
||||||
"""Call action with right context."""
|
"""Call action with right context."""
|
||||||
action({
|
hass.async_run_job(action, {
|
||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'state',
|
'platform': 'state',
|
||||||
'entity_id': entity,
|
'entity_id': entity,
|
||||||
@ -61,35 +64,37 @@ def trigger(hass, config, action):
|
|||||||
call_action()
|
call_action()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@callback
|
||||||
def state_for_listener(now):
|
def state_for_listener(now):
|
||||||
"""Fire on state changes after a delay and calls action."""
|
"""Fire on state changes after a delay and calls action."""
|
||||||
remove_state_for_cancel()
|
async_remove_state_for_cancel()
|
||||||
call_action()
|
call_action()
|
||||||
|
|
||||||
|
@callback
|
||||||
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
|
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
|
||||||
"""Fire on changes and cancel for listener if changed."""
|
"""Fire on changes and cancel for listener if changed."""
|
||||||
if inner_to_s.state == to_s.state:
|
if inner_to_s.state == to_s.state:
|
||||||
return
|
return
|
||||||
remove_state_for_listener()
|
async_remove_state_for_listener()
|
||||||
remove_state_for_cancel()
|
async_remove_state_for_cancel()
|
||||||
|
|
||||||
remove_state_for_listener = track_point_in_time(
|
async_remove_state_for_listener = async_track_point_in_utc_time(
|
||||||
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
||||||
|
|
||||||
remove_state_for_cancel = track_state_change(
|
async_remove_state_for_cancel = async_track_state_change(
|
||||||
hass, entity, state_for_cancel_listener)
|
hass, entity, state_for_cancel_listener)
|
||||||
|
|
||||||
unsub = track_state_change(hass, entity_id, state_automation_listener,
|
unsub = async_track_state_change(
|
||||||
from_state, to_state)
|
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||||
|
|
||||||
def remove():
|
def async_remove():
|
||||||
"""Remove state listeners."""
|
"""Remove state listeners async."""
|
||||||
unsub()
|
unsub()
|
||||||
# pylint: disable=not-callable
|
# pylint: disable=not-callable
|
||||||
if remove_state_for_cancel is not None:
|
if async_remove_state_for_cancel is not None:
|
||||||
remove_state_for_cancel()
|
async_remove_state_for_cancel()
|
||||||
|
|
||||||
if remove_state_for_listener is not None:
|
if async_remove_state_for_listener is not None:
|
||||||
remove_state_for_listener()
|
async_remove_state_for_listener()
|
||||||
|
|
||||||
return remove
|
return async_remove
|
||||||
|
@ -9,9 +9,10 @@ import logging
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE)
|
CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE)
|
||||||
from homeassistant.helpers.event import track_sunrise, track_sunset
|
from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DEPENDENCIES = ['sun']
|
DEPENDENCIES = ['sun']
|
||||||
@ -25,14 +26,15 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def trigger(hass, config, action):
|
def async_trigger(hass, config, action):
|
||||||
"""Listen for events based on configuration."""
|
"""Listen for events based on configuration."""
|
||||||
event = config.get(CONF_EVENT)
|
event = config.get(CONF_EVENT)
|
||||||
offset = config.get(CONF_OFFSET)
|
offset = config.get(CONF_OFFSET)
|
||||||
|
|
||||||
|
@callback
|
||||||
def call_action():
|
def call_action():
|
||||||
"""Call action with right context."""
|
"""Call action with right context."""
|
||||||
action({
|
hass.async_run_job(action, {
|
||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'sun',
|
'platform': 'sun',
|
||||||
'event': event,
|
'event': event,
|
||||||
@ -42,6 +44,6 @@ def trigger(hass, config, action):
|
|||||||
|
|
||||||
# Do something to call action
|
# Do something to call action
|
||||||
if event == SUN_EVENT_SUNRISE:
|
if event == SUN_EVENT_SUNRISE:
|
||||||
return track_sunrise(hass, call_action, offset)
|
return async_track_sunrise(hass, call_action, offset)
|
||||||
else:
|
else:
|
||||||
return track_sunset(hass, call_action, offset)
|
return async_track_sunset(hass, call_action, offset)
|
||||||
|
@ -4,14 +4,14 @@ Offer template automation rules.
|
|||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#template-trigger
|
at https://home-assistant.io/components/automation/#template-trigger
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM
|
from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM
|
||||||
from homeassistant.helpers import condition
|
from homeassistant.helpers import condition
|
||||||
from homeassistant.helpers.event import track_state_change
|
from homeassistant.helpers.event import async_track_state_change
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def trigger(hass, config, action):
|
def async_trigger(hass, config, action):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
@ -31,7 +31,7 @@ def trigger(hass, config, action):
|
|||||||
# Local variable to keep track of if the action has already been triggered
|
# Local variable to keep track of if the action has already been triggered
|
||||||
already_triggered = False
|
already_triggered = False
|
||||||
|
|
||||||
@asyncio.coroutine
|
@callback
|
||||||
def state_changed_listener(entity_id, from_s, to_s):
|
def state_changed_listener(entity_id, from_s, to_s):
|
||||||
"""Listen for state changes and calls action."""
|
"""Listen for state changes and calls action."""
|
||||||
nonlocal already_triggered
|
nonlocal already_triggered
|
||||||
@ -40,7 +40,7 @@ def trigger(hass, config, action):
|
|||||||
# Check to see if template returns true
|
# Check to see if template returns true
|
||||||
if template_result and not already_triggered:
|
if template_result and not already_triggered:
|
||||||
already_triggered = True
|
already_triggered = True
|
||||||
hass.async_add_job(action, {
|
hass.async_run_job(action, {
|
||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'template',
|
'platform': 'template',
|
||||||
'entity_id': entity_id,
|
'entity_id': entity_id,
|
||||||
@ -51,5 +51,5 @@ def trigger(hass, config, action):
|
|||||||
elif not template_result:
|
elif not template_result:
|
||||||
already_triggered = False
|
already_triggered = False
|
||||||
|
|
||||||
return track_state_change(hass, value_template.extract_entities(),
|
return async_track_state_change(hass, value_template.extract_entities(),
|
||||||
state_changed_listener)
|
state_changed_listener)
|
||||||
|
@ -8,9 +8,10 @@ import logging
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import CONF_AFTER, CONF_PLATFORM
|
from homeassistant.const import CONF_AFTER, CONF_PLATFORM
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.event import track_time_change
|
from homeassistant.helpers.event import async_track_time_change
|
||||||
|
|
||||||
CONF_HOURS = "hours"
|
CONF_HOURS = "hours"
|
||||||
CONF_MINUTES = "minutes"
|
CONF_MINUTES = "minutes"
|
||||||
@ -28,7 +29,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({
|
|||||||
CONF_SECONDS, CONF_AFTER))
|
CONF_SECONDS, CONF_AFTER))
|
||||||
|
|
||||||
|
|
||||||
def trigger(hass, config, action):
|
def async_trigger(hass, config, action):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
if CONF_AFTER in config:
|
if CONF_AFTER in config:
|
||||||
after = config.get(CONF_AFTER)
|
after = config.get(CONF_AFTER)
|
||||||
@ -38,14 +39,15 @@ def trigger(hass, config, action):
|
|||||||
minutes = config.get(CONF_MINUTES)
|
minutes = config.get(CONF_MINUTES)
|
||||||
seconds = config.get(CONF_SECONDS)
|
seconds = config.get(CONF_SECONDS)
|
||||||
|
|
||||||
|
@callback
|
||||||
def time_automation_listener(now):
|
def time_automation_listener(now):
|
||||||
"""Listen for time changes and calls action."""
|
"""Listen for time changes and calls action."""
|
||||||
action({
|
hass.async_run_job(action, {
|
||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'time',
|
'platform': 'time',
|
||||||
'now': now,
|
'now': now,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return track_time_change(hass, time_automation_listener,
|
return async_track_time_change(hass, time_automation_listener,
|
||||||
hour=hours, minute=minutes, second=seconds)
|
hour=hours, minute=minutes, second=seconds)
|
||||||
|
@ -6,9 +6,10 @@ at https://home-assistant.io/components/automation/#zone-trigger
|
|||||||
"""
|
"""
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_EVENT, CONF_ENTITY_ID, CONF_ZONE, MATCH_ALL, CONF_PLATFORM)
|
CONF_EVENT, CONF_ENTITY_ID, CONF_ZONE, MATCH_ALL, CONF_PLATFORM)
|
||||||
from homeassistant.helpers.event import track_state_change
|
from homeassistant.helpers.event import async_track_state_change
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
condition, config_validation as cv, location)
|
condition, config_validation as cv, location)
|
||||||
|
|
||||||
@ -25,12 +26,13 @@ TRIGGER_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def trigger(hass, config, action):
|
def async_trigger(hass, config, action):
|
||||||
"""Listen for state changes based on configuration."""
|
"""Listen for state changes based on configuration."""
|
||||||
entity_id = config.get(CONF_ENTITY_ID)
|
entity_id = config.get(CONF_ENTITY_ID)
|
||||||
zone_entity_id = config.get(CONF_ZONE)
|
zone_entity_id = config.get(CONF_ZONE)
|
||||||
event = config.get(CONF_EVENT)
|
event = config.get(CONF_EVENT)
|
||||||
|
|
||||||
|
@callback
|
||||||
def zone_automation_listener(entity, from_s, to_s):
|
def zone_automation_listener(entity, from_s, to_s):
|
||||||
"""Listen for state changes and calls action."""
|
"""Listen for state changes and calls action."""
|
||||||
if from_s and not location.has_location(from_s) or \
|
if from_s and not location.has_location(from_s) or \
|
||||||
@ -47,7 +49,7 @@ def trigger(hass, config, action):
|
|||||||
# pylint: disable=too-many-boolean-expressions
|
# pylint: disable=too-many-boolean-expressions
|
||||||
if event == EVENT_ENTER and not from_match and to_match or \
|
if event == EVENT_ENTER and not from_match and to_match or \
|
||||||
event == EVENT_LEAVE and from_match and not to_match:
|
event == EVENT_LEAVE and from_match and not to_match:
|
||||||
action({
|
hass.async_run_job(action, {
|
||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'zone',
|
'platform': 'zone',
|
||||||
'entity_id': entity,
|
'entity_id': entity,
|
||||||
@ -58,5 +60,5 @@ def trigger(hass, config, action):
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return track_state_change(hass, entity_id, zone_automation_listener,
|
return async_track_state_change(hass, entity_id, zone_automation_listener,
|
||||||
MATCH_ALL, MATCH_ALL)
|
MATCH_ALL, MATCH_ALL)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Support for exposed aREST RESTful API of a device.
|
Support for an exposed aREST RESTful API of a device.
|
||||||
|
|
||||||
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/binary_sensor.arest/
|
https://home-assistant.io/components/binary_sensor.arest/
|
||||||
@ -8,31 +8,32 @@ import logging
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, SENSOR_CLASSES)
|
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
|
||||||
from homeassistant.const import CONF_RESOURCE, CONF_PIN
|
from homeassistant.const import (
|
||||||
|
CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_SENSOR_CLASS)
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_RESOURCE): cv.url,
|
||||||
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
|
vol.Required(CONF_PIN): cv.string,
|
||||||
|
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the aREST binary sensor."""
|
"""Setup the aREST binary sensor."""
|
||||||
resource = config.get(CONF_RESOURCE)
|
resource = config.get(CONF_RESOURCE)
|
||||||
pin = config.get(CONF_PIN)
|
pin = config.get(CONF_PIN)
|
||||||
|
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||||
sensor_class = config.get('sensor_class')
|
|
||||||
if sensor_class not in SENSOR_CLASSES:
|
|
||||||
_LOGGER.warning('Unknown sensor class: %s', sensor_class)
|
|
||||||
sensor_class = None
|
|
||||||
|
|
||||||
if None in (resource, pin):
|
|
||||||
_LOGGER.error('Not all required config keys present: %s',
|
|
||||||
', '.join((CONF_RESOURCE, CONF_PIN)))
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(resource, timeout=10).json()
|
response = requests.get(resource, timeout=10).json()
|
||||||
@ -49,11 +50,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
arest = ArestData(resource, pin)
|
arest = ArestData(resource, pin)
|
||||||
|
|
||||||
add_devices([ArestBinarySensor(
|
add_devices([ArestBinarySensor(
|
||||||
arest,
|
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
||||||
resource,
|
sensor_class, pin)])
|
||||||
config.get('name', response['name']),
|
|
||||||
sensor_class,
|
|
||||||
pin)])
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||||
|
91
homeassistant/components/binary_sensor/digital_ocean.py
Normal file
91
homeassistant/components/binary_sensor/digital_ocean.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
"""
|
||||||
|
Support for monitoring the state of Digital Ocean droplets.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/binary_sensor.digital_ocean/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||||
|
from homeassistant.components.digital_ocean import (
|
||||||
|
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
|
||||||
|
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
|
||||||
|
ATTR_REGION, ATTR_VCPUS)
|
||||||
|
from homeassistant.loader import get_component
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'Droplet'
|
||||||
|
DEFAULT_SENSOR_CLASS = 'motion'
|
||||||
|
DEPENDENCIES = ['digital_ocean']
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the Digital Ocean droplet sensor."""
|
||||||
|
digital_ocean = get_component('digital_ocean')
|
||||||
|
droplets = config.get(CONF_DROPLETS)
|
||||||
|
|
||||||
|
dev = []
|
||||||
|
for droplet in droplets:
|
||||||
|
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
|
||||||
|
dev.append(DigitalOceanBinarySensor(
|
||||||
|
digital_ocean.DIGITAL_OCEAN, droplet_id))
|
||||||
|
|
||||||
|
add_devices(dev)
|
||||||
|
|
||||||
|
|
||||||
|
class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||||
|
"""Representation of a Digital Ocean droplet sensor."""
|
||||||
|
|
||||||
|
def __init__(self, do, droplet_id):
|
||||||
|
"""Initialize a new Digital Ocean sensor."""
|
||||||
|
self._digital_ocean = do
|
||||||
|
self._droplet_id = droplet_id
|
||||||
|
self._state = None
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
return self.data.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if the binary sensor is on."""
|
||||||
|
return self.data.status == 'active'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sensor_class(self):
|
||||||
|
"""Return the class of this sensor."""
|
||||||
|
return DEFAULT_SENSOR_CLASS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
"""Return the state attributes of the Digital Ocean droplet."""
|
||||||
|
return {
|
||||||
|
ATTR_CREATED_AT: self.data.created_at,
|
||||||
|
ATTR_DROPLET_ID: self.data.id,
|
||||||
|
ATTR_DROPLET_NAME: self.data.name,
|
||||||
|
ATTR_FEATURES: self.data.features,
|
||||||
|
ATTR_IPV4_ADDRESS: self.data.ip_address,
|
||||||
|
ATTR_IPV6_ADDRESS: self.data.ip_v6_address,
|
||||||
|
ATTR_MEMORY: self.data.memory,
|
||||||
|
ATTR_REGION: self.data.region['name'],
|
||||||
|
ATTR_VCPUS: self.data.vcpus,
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Update state of sensor."""
|
||||||
|
self._digital_ocean.update()
|
||||||
|
|
||||||
|
for droplet in self._digital_ocean.data:
|
||||||
|
if droplet.id == self._droplet_id:
|
||||||
|
self.data = droplet
|
@ -16,6 +16,7 @@ DEPENDENCIES = ['homematic']
|
|||||||
SENSOR_TYPES_CLASS = {
|
SENSOR_TYPES_CLASS = {
|
||||||
"Remote": None,
|
"Remote": None,
|
||||||
"ShutterContact": "opening",
|
"ShutterContact": "opening",
|
||||||
|
"IPShutterContact": "opening",
|
||||||
"Smoke": "smoke",
|
"Smoke": "smoke",
|
||||||
"SmokeV2": "smoke",
|
"SmokeV2": "smoke",
|
||||||
"Motion": "motion",
|
"Motion": "motion",
|
||||||
|
@ -1,41 +1,56 @@
|
|||||||
"""
|
"""
|
||||||
Support for exposing nx584 elements as sensors.
|
Support for exposing NX584 elements as sensors.
|
||||||
|
|
||||||
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.nx584/
|
https://home-assistant.io/components/binary_sensor.nx584/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
SENSOR_CLASSES, BinarySensorDevice)
|
SENSOR_CLASSES, BinarySensorDevice, PLATFORM_SCHEMA)
|
||||||
|
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pynx584==0.2']
|
REQUIREMENTS = ['pynx584==0.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_EXCLUDE_ZONES = 'exclude_zones'
|
||||||
|
CONF_ZONE_TYPES = 'zone_types'
|
||||||
|
|
||||||
|
DEFAULT_HOST = 'localhost'
|
||||||
|
DEFAULT_PORT = '5007'
|
||||||
|
DEFAULT_SSL = False
|
||||||
|
|
||||||
|
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||||
|
cv.positive_int: vol.In(SENSOR_CLASSES),
|
||||||
|
})
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_EXCLUDE_ZONES, default=[]):
|
||||||
|
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||||
|
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||||
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
|
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup nx584 binary sensor platform."""
|
"""Setup the NX584 binary sensor platform."""
|
||||||
from nx584 import client as nx584_client
|
from nx584 import client as nx584_client
|
||||||
|
|
||||||
host = config.get('host', 'localhost:5007')
|
host = config.get(CONF_HOST)
|
||||||
exclude = config.get('exclude_zones', [])
|
port = config.get(CONF_PORT)
|
||||||
zone_types = config.get('zone_types', {})
|
exclude = config.get(CONF_EXCLUDE_ZONES)
|
||||||
|
zone_types = config.get(CONF_ZONE_TYPES)
|
||||||
if not all(isinstance(zone, int) for zone in exclude):
|
|
||||||
_LOGGER.error('Invalid excluded zone specified (use zone number)')
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not all(isinstance(zone, int) and ztype in SENSOR_CLASSES
|
|
||||||
for zone, ztype in zone_types.items()):
|
|
||||||
_LOGGER.error('Invalid zone_types entry')
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = nx584_client.Client('http://%s' % host)
|
client = nx584_client.Client('http://{}:{}'.format(host, port))
|
||||||
zones = client.list_zones()
|
zones = client.list_zones()
|
||||||
except requests.exceptions.ConnectionError as ex:
|
except requests.exceptions.ConnectionError as ex:
|
||||||
_LOGGER.error('Unable to connect to NX584: %s', str(ex))
|
_LOGGER.error('Unable to connect to NX584: %s', str(ex))
|
||||||
@ -43,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
version = [int(v) for v in client.get_version().split('.')]
|
version = [int(v) for v in client.get_version().split('.')]
|
||||||
if version < [1, 1]:
|
if version < [1, 1]:
|
||||||
_LOGGER.error('NX584 is too old to use for sensors (>=0.2 required)')
|
_LOGGER.error("NX584 is too old to use for sensors (>=0.2 required)")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
zone_sensors = {
|
zone_sensors = {
|
||||||
@ -57,13 +72,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
watcher = NX584Watcher(client, zone_sensors)
|
watcher = NX584Watcher(client, zone_sensors)
|
||||||
watcher.start()
|
watcher.start()
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning('No zones found on NX584')
|
_LOGGER.warning("No zones found on NX584")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class NX584ZoneSensor(BinarySensorDevice):
|
class NX584ZoneSensor(BinarySensorDevice):
|
||||||
"""Represents a NX584 zone as a sensor."""
|
"""Representation of a NX584 zone as a sensor."""
|
||||||
|
|
||||||
def __init__(self, zone, zone_type):
|
def __init__(self, zone, zone_type):
|
||||||
"""Initialize the nx594 binary sensor."""
|
"""Initialize the nx594 binary sensor."""
|
||||||
@ -96,7 +110,7 @@ class NX584Watcher(threading.Thread):
|
|||||||
"""Event listener thread to process NX584 events."""
|
"""Event listener thread to process NX584 events."""
|
||||||
|
|
||||||
def __init__(self, client, zone_sensors):
|
def __init__(self, client, zone_sensors):
|
||||||
"""Initialize nx584 watcher thread."""
|
"""Initialize NX584 watcher thread."""
|
||||||
super(NX584Watcher, self).__init__()
|
super(NX584Watcher, self).__init__()
|
||||||
self.daemon = True
|
self.daemon = True
|
||||||
self._client = client
|
self._client = client
|
||||||
@ -130,5 +144,5 @@ class NX584Watcher(threading.Thread):
|
|||||||
try:
|
try:
|
||||||
self._run()
|
self._run()
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
_LOGGER.error('Failed to reach NX584 server')
|
_LOGGER.error("Failed to reach NX584 server")
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
|
@ -5,15 +5,19 @@ For more details about this platform, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/binary_sensor.rest/
|
https://home-assistant.io/components/binary_sensor.rest/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA)
|
BinarySensorDevice, SENSOR_CLASSES_SCHEMA, PLATFORM_SCHEMA)
|
||||||
from homeassistant.components.sensor.rest import RestData
|
from homeassistant.components.sensor.rest import RestData
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE,
|
CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE,
|
||||||
CONF_SENSOR_CLASS, CONF_VERIFY_SSL)
|
CONF_SENSOR_CLASS, CONF_VERIFY_SSL, CONF_USERNAME, CONF_PASSWORD,
|
||||||
|
CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
|
||||||
|
HTTP_DIGEST_AUTHENTICATION)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -24,16 +28,21 @@ DEFAULT_VERIFY_SSL = True
|
|||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_RESOURCE): cv.url,
|
vol.Required(CONF_RESOURCE): cv.url,
|
||||||
|
vol.Optional(CONF_AUTHENTICATION):
|
||||||
|
vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]),
|
||||||
|
vol.Optional(CONF_HEADERS): cv.string,
|
||||||
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET']),
|
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET']),
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_PAYLOAD): cv.string,
|
vol.Optional(CONF_PAYLOAD): cv.string,
|
||||||
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
|
vol.Optional(CONF_SENSOR_CLASS): SENSOR_CLASSES_SCHEMA,
|
||||||
|
vol.Optional(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
|
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable, too-many-locals
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the REST binary sensor."""
|
"""Setup the REST binary sensor."""
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
@ -41,11 +50,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
method = config.get(CONF_METHOD)
|
method = config.get(CONF_METHOD)
|
||||||
payload = config.get(CONF_PAYLOAD)
|
payload = config.get(CONF_PAYLOAD)
|
||||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||||
|
username = config.get(CONF_USERNAME)
|
||||||
|
password = config.get(CONF_PASSWORD)
|
||||||
|
headers = json.loads(config.get(CONF_HEADERS, '{}'))
|
||||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
rest = RestData(method, resource, payload, verify_ssl)
|
|
||||||
|
if username and password:
|
||||||
|
if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION:
|
||||||
|
auth = HTTPDigestAuth(username, password)
|
||||||
|
else:
|
||||||
|
auth = HTTPBasicAuth(username, password)
|
||||||
|
else:
|
||||||
|
auth = None
|
||||||
|
|
||||||
|
rest = RestData(method, resource, auth, headers, payload, verify_ssl)
|
||||||
rest.update()
|
rest.update()
|
||||||
|
|
||||||
if rest.data is None:
|
if rest.data is None:
|
||||||
|
@ -4,10 +4,12 @@ Support for exposing a templated binary sensor.
|
|||||||
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/binary_sensor.template/
|
https://home-assistant.io/components/binary_sensor.template/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
|
BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA,
|
||||||
SENSOR_CLASSES_SCHEMA)
|
SENSOR_CLASSES_SCHEMA)
|
||||||
@ -81,9 +83,10 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
@callback
|
||||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||||
"""Called when the target device changes state."""
|
"""Called when the target device changes state."""
|
||||||
self.update_ha_state(True)
|
hass.loop.create_task(self.async_update_ha_state(True))
|
||||||
|
|
||||||
track_state_change(hass, entity_ids, template_bsensor_state_listener)
|
track_state_change(hass, entity_ids, template_bsensor_state_listener)
|
||||||
|
|
||||||
@ -107,10 +110,11 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||||||
"""No polling needed."""
|
"""No polling needed."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update(self):
|
@asyncio.coroutine
|
||||||
|
def async_update(self):
|
||||||
"""Get the latest data and update the state."""
|
"""Get the latest data and update the state."""
|
||||||
try:
|
try:
|
||||||
self._state = self._template.render().lower() == 'true'
|
self._state = self._template.async_render().lower() == 'true'
|
||||||
except TemplateError as ex:
|
except TemplateError as ex:
|
||||||
if ex.args and ex.args[0].startswith(
|
if ex.args and ex.args[0].startswith(
|
||||||
"UndefinedError: 'None' has no attribute"):
|
"UndefinedError: 'None' has no attribute"):
|
||||||
|
@ -20,7 +20,10 @@ SENSOR_TYPES = {
|
|||||||
"vibration": "vibration",
|
"vibration": "vibration",
|
||||||
"loudness": "sound",
|
"loudness": "sound",
|
||||||
"liquid_detected": "moisture",
|
"liquid_detected": "moisture",
|
||||||
"motion": "motion"
|
"motion": "motion",
|
||||||
|
"presence": "occupancy",
|
||||||
|
"co_detected": "gas",
|
||||||
|
"smoke_detected": "smoke"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -35,6 +38,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
for key in pywink.get_keys():
|
for key in pywink.get_keys():
|
||||||
add_devices([WinkBinarySensorDevice(key)])
|
add_devices([WinkBinarySensorDevice(key)])
|
||||||
|
|
||||||
|
for sensor in pywink.get_smoke_and_co_detectors():
|
||||||
|
add_devices([WinkBinarySensorDevice(sensor)])
|
||||||
|
|
||||||
|
|
||||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||||
"""Representation of a Wink binary sensor."""
|
"""Representation of a Wink binary sensor."""
|
||||||
@ -58,17 +64,25 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
|||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if the binary sensor is on."""
|
"""Return true if the binary sensor is on."""
|
||||||
if self.capability == "loudness":
|
if self.capability == "loudness":
|
||||||
return self.wink.loudness_boolean()
|
state = self.wink.loudness_boolean()
|
||||||
elif self.capability == "vibration":
|
elif self.capability == "vibration":
|
||||||
return self.wink.vibration_boolean()
|
state = self.wink.vibration_boolean()
|
||||||
elif self.capability == "brightness":
|
elif self.capability == "brightness":
|
||||||
return self.wink.brightness_boolean()
|
state = self.wink.brightness_boolean()
|
||||||
elif self.capability == "liquid_detected":
|
elif self.capability == "liquid_detected":
|
||||||
return self.wink.liquid_boolean()
|
state = self.wink.liquid_boolean()
|
||||||
elif self.capability == "motion":
|
elif self.capability == "motion":
|
||||||
return self.wink.motion_boolean()
|
state = self.wink.motion_boolean()
|
||||||
|
elif self.capability == "presence":
|
||||||
|
state = self.wink.presence_boolean()
|
||||||
|
elif self.capability == "co_detected":
|
||||||
|
state = self.wink.co_detected_boolean()
|
||||||
|
elif self.capability == "smoke_detected":
|
||||||
|
state = self.wink.smoke_detected_boolean()
|
||||||
else:
|
else:
|
||||||
return self.wink.state()
|
state = self.wink.state()
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sensor_class(self):
|
def sensor_class(self):
|
||||||
|
@ -36,8 +36,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None or zwave.NETWORK is None:
|
if discovery_info is None or zwave.NETWORK is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
|
|
||||||
# Make sure that we have values for the key before converting to int
|
# Make sure that we have values for the key before converting to int
|
||||||
@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
])
|
])
|
||||||
return
|
return
|
||||||
|
|
||||||
if value.command_class == zwave.COMMAND_CLASS_SENSOR_BINARY:
|
if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||||
add_devices([ZWaveBinarySensor(value, None)])
|
add_devices([ZWaveBinarySensor(value, None)])
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,6 +58,12 @@ ATTR_OPERATION_LIST = "operation_list"
|
|||||||
ATTR_SWING_MODE = "swing_mode"
|
ATTR_SWING_MODE = "swing_mode"
|
||||||
ATTR_SWING_LIST = "swing_list"
|
ATTR_SWING_LIST = "swing_list"
|
||||||
|
|
||||||
|
CONVERTIBLE_ATTRIBUTE = [
|
||||||
|
ATTR_TEMPERATURE,
|
||||||
|
ATTR_TARGET_TEMP_LOW,
|
||||||
|
ATTR_TARGET_TEMP_HIGH,
|
||||||
|
]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SET_AWAY_MODE_SCHEMA = vol.Schema({
|
SET_AWAY_MODE_SCHEMA = vol.Schema({
|
||||||
@ -73,6 +79,7 @@ SET_TEMPERATURE_SCHEMA = vol.Schema({
|
|||||||
vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float),
|
vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float),
|
||||||
vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float),
|
vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float),
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
|
vol.Optional(ATTR_OPERATION_MODE): cv.string,
|
||||||
})
|
})
|
||||||
SET_FAN_MODE_SCHEMA = vol.Schema({
|
SET_FAN_MODE_SCHEMA = vol.Schema({
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
@ -116,8 +123,10 @@ def set_aux_heat(hass, aux_heat, entity_id=None):
|
|||||||
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
|
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
def set_temperature(hass, temperature=None, entity_id=None,
|
def set_temperature(hass, temperature=None, entity_id=None,
|
||||||
target_temp_high=None, target_temp_low=None):
|
target_temp_high=None, target_temp_low=None,
|
||||||
|
operation_mode=None):
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
kwargs = {
|
kwargs = {
|
||||||
key: value for key, value in [
|
key: value for key, value in [
|
||||||
@ -125,6 +134,7 @@ def set_temperature(hass, temperature=None, entity_id=None,
|
|||||||
(ATTR_TARGET_TEMP_HIGH, target_temp_high),
|
(ATTR_TARGET_TEMP_HIGH, target_temp_high),
|
||||||
(ATTR_TARGET_TEMP_LOW, target_temp_low),
|
(ATTR_TARGET_TEMP_LOW, target_temp_low),
|
||||||
(ATTR_ENTITY_ID, entity_id),
|
(ATTR_ENTITY_ID, entity_id),
|
||||||
|
(ATTR_OPERATION_MODE, operation_mode)
|
||||||
] if value is not None
|
] if value is not None
|
||||||
}
|
}
|
||||||
_LOGGER.debug("set_temperature start data=%s", kwargs)
|
_LOGGER.debug("set_temperature start data=%s", kwargs)
|
||||||
@ -235,10 +245,20 @@ def setup(hass, config):
|
|||||||
def temperature_set_service(service):
|
def temperature_set_service(service):
|
||||||
"""Set temperature on the target climate devices."""
|
"""Set temperature on the target climate devices."""
|
||||||
target_climate = component.extract_from_service(service)
|
target_climate = component.extract_from_service(service)
|
||||||
kwargs = service.data
|
|
||||||
for climate in target_climate:
|
|
||||||
climate.set_temperature(**kwargs)
|
|
||||||
|
|
||||||
|
for climate in target_climate:
|
||||||
|
kwargs = {}
|
||||||
|
for value, temp in service.data.items():
|
||||||
|
if value in CONVERTIBLE_ATTRIBUTE:
|
||||||
|
kwargs[value] = convert_temperature(
|
||||||
|
temp,
|
||||||
|
hass.config.units.temperature_unit,
|
||||||
|
climate.unit_of_measurement
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
kwargs[value] = temp
|
||||||
|
|
||||||
|
climate.set_temperature(**kwargs)
|
||||||
if climate.should_poll:
|
if climate.should_poll:
|
||||||
climate.update_ha_state(True)
|
climate.update_ha_state(True)
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ from homeassistant.components.climate import (
|
|||||||
ATTR_TEMPERATURE)
|
ATTR_TEMPERATURE)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
TEMP_CELSIUS, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
TEMP_CELSIUS, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
|
||||||
from homeassistant.util.temperature import convert as convert_temperature
|
|
||||||
|
|
||||||
DEPENDENCIES = ['nest']
|
DEPENDENCIES = ['nest']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -127,12 +126,9 @@ class NestThermostat(ClimateDevice):
|
|||||||
|
|
||||||
def set_temperature(self, **kwargs):
|
def set_temperature(self, **kwargs):
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
if kwargs.get(ATTR_TARGET_TEMP_LOW) is not None and \
|
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||||
kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None:
|
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||||
target_temp_high = convert_temperature(kwargs.get(
|
if target_temp_low is not None and target_temp_high is not None:
|
||||||
ATTR_TARGET_TEMP_HIGH), self._unit, TEMP_CELSIUS)
|
|
||||||
target_temp_low = convert_temperature(kwargs.get(
|
|
||||||
ATTR_TARGET_TEMP_LOW), self._unit, TEMP_CELSIUS)
|
|
||||||
|
|
||||||
if self.device.mode == 'range':
|
if self.device.mode == 'range':
|
||||||
temp = (target_temp_low, target_temp_high)
|
temp = (target_temp_low, target_temp_high)
|
||||||
|
@ -34,6 +34,18 @@ set_temperature:
|
|||||||
description: New target temperature for hvac
|
description: New target temperature for hvac
|
||||||
example: 25
|
example: 25
|
||||||
|
|
||||||
|
target_temp_high:
|
||||||
|
description: New target high tempereature for hvac
|
||||||
|
example: 26
|
||||||
|
|
||||||
|
target_temp_low:
|
||||||
|
description: New target low temperature for hvac
|
||||||
|
example: 20
|
||||||
|
|
||||||
|
operation_mode:
|
||||||
|
description: Operation mode to set temperature to. This defaults to current_operation mode if not set, or set incorrectly.
|
||||||
|
example: 'Heat'
|
||||||
|
|
||||||
set_humidity:
|
set_humidity:
|
||||||
description: Set target humidity of climate device
|
description: Set target humidity of climate device
|
||||||
|
|
||||||
|
@ -8,9 +8,9 @@ https://home-assistant.io/components/climate.zwave/
|
|||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import logging
|
import logging
|
||||||
from homeassistant.components.climate import DOMAIN
|
from homeassistant.components.climate import DOMAIN
|
||||||
from homeassistant.components.climate import ClimateDevice
|
from homeassistant.components.climate import (
|
||||||
from homeassistant.components.zwave import (
|
ClimateDevice, ATTR_OPERATION_MODE)
|
||||||
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
|
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||||
from homeassistant.components import zwave
|
from homeassistant.components import zwave
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||||
@ -28,12 +28,6 @@ HORSTMANN = 0x0059
|
|||||||
HORSTMANN_HRT4_ZW = 0x3
|
HORSTMANN_HRT4_ZW = 0x3
|
||||||
HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW)
|
HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW)
|
||||||
|
|
||||||
COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31
|
|
||||||
COMMAND_CLASS_THERMOSTAT_MODE = 0x40
|
|
||||||
COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43
|
|
||||||
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44
|
|
||||||
COMMAND_CLASS_CONFIGURATION = 0x70
|
|
||||||
|
|
||||||
WORKAROUND_ZXT_120 = 'zxt_120'
|
WORKAROUND_ZXT_120 = 'zxt_120'
|
||||||
WORKAROUND_HRT4_ZW = 'hrt4_zw'
|
WORKAROUND_HRT4_ZW = 'hrt4_zw'
|
||||||
|
|
||||||
@ -67,19 +61,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
discovery_info, zwave.NETWORK)
|
discovery_info, zwave.NETWORK)
|
||||||
return
|
return
|
||||||
temp_unit = hass.config.units.temperature_unit
|
temp_unit = hass.config.units.temperature_unit
|
||||||
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
|
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||||
value = node.values[discovery_info[ATTR_VALUE_ID]]
|
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
add_devices([ZWaveClimate(value, temp_unit)])
|
add_devices([ZWaveClimate(value, temp_unit)])
|
||||||
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||||
discovery_info, zwave.NETWORK)
|
discovery_info, zwave.NETWORK)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, abstract-method
|
# pylint: disable=abstract-method
|
||||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||||
"""Represents a ZWave Climate device."""
|
"""Represents a ZWave Climate device."""
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods, too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
def __init__(self, value, temp_unit):
|
def __init__(self, value, temp_unit):
|
||||||
"""Initialize the zwave climate device."""
|
"""Initialize the zwave climate device."""
|
||||||
from openzwave.network import ZWaveNetwork
|
from openzwave.network import ZWaveNetwork
|
||||||
@ -130,7 +124,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
"""Callback on data change for the registered node/value pair."""
|
"""Callback on data change for the registered node/value pair."""
|
||||||
# Operation Mode
|
# Operation Mode
|
||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||||
self._current_operation = value.data
|
self._current_operation = value.data
|
||||||
self._index_operation = SET_TEMP_TO_INDEX.get(
|
self._index_operation = SET_TEMP_TO_INDEX.get(
|
||||||
self._current_operation)
|
self._current_operation)
|
||||||
@ -139,14 +133,16 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
_LOGGER.debug("self._current_operation=%s",
|
_LOGGER.debug("self._current_operation=%s",
|
||||||
self._current_operation)
|
self._current_operation)
|
||||||
# Current Temp
|
# Current Temp
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values():
|
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||||
|
.values()):
|
||||||
if value.label == 'Temperature':
|
if value.label == 'Temperature':
|
||||||
self._current_temperature = int(value.data)
|
self._current_temperature = int(value.data)
|
||||||
self._unit = value.units
|
self._unit = value.units
|
||||||
# Fan Mode
|
# Fan Mode
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||||
|
.values()):
|
||||||
self._current_fan_mode = value.data
|
self._current_fan_mode = value.data
|
||||||
self._fan_list = list(value.data_items)
|
self._fan_list = list(value.data_items)
|
||||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||||
@ -154,17 +150,27 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
self._current_fan_mode)
|
self._current_fan_mode)
|
||||||
# Swing mode
|
# Swing mode
|
||||||
if self._zxt_120 == 1:
|
if self._zxt_120 == 1:
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||||
if value.command_class == 112 and value.index == 33:
|
.values()):
|
||||||
|
if value.command_class == \
|
||||||
|
zwave.const.COMMAND_CLASS_CONFIGURATION and \
|
||||||
|
value.index == 33:
|
||||||
self._current_swing_mode = value.data
|
self._current_swing_mode = value.data
|
||||||
self._swing_list = list(value.data_items)
|
self._swing_list = list(value.data_items)
|
||||||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||||
_LOGGER.debug("self._current_swing_mode=%s",
|
_LOGGER.debug("self._current_swing_mode=%s",
|
||||||
self._current_swing_mode)
|
self._current_swing_mode)
|
||||||
# Set point
|
# Set point
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||||
|
.values()):
|
||||||
|
if value.data == 0:
|
||||||
|
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||||
|
"current_temperature=%s",
|
||||||
|
self._current_temperature)
|
||||||
|
self._target_temperature = int(self._current_temperature)
|
||||||
|
break
|
||||||
if self.current_operation is not None and \
|
if self.current_operation is not None and \
|
||||||
self.current_operation != 'Off':
|
self.current_operation != 'Off':
|
||||||
if self._index_operation != value.index:
|
if self._index_operation != value.index:
|
||||||
@ -232,16 +238,26 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
return self._target_temperature
|
return self._target_temperature
|
||||||
|
|
||||||
# pylint: disable=too-many-branches, too-many-statements
|
|
||||||
def set_temperature(self, **kwargs):
|
def set_temperature(self, **kwargs):
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
operation_mode = kwargs.get(ATTR_OPERATION_MODE)
|
||||||
|
_LOGGER.debug("set_temperature operation_mode=%s", operation_mode)
|
||||||
|
|
||||||
|
for value in (self._node.get_values(
|
||||||
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||||
|
.values()):
|
||||||
|
if operation_mode is not None:
|
||||||
|
setpoint_mode = SET_TEMP_TO_INDEX.get(operation_mode)
|
||||||
|
if value.index != setpoint_mode:
|
||||||
|
continue
|
||||||
|
_LOGGER.debug("setpoint_mode=%s", setpoint_mode)
|
||||||
|
value.data = temperature
|
||||||
|
break
|
||||||
|
|
||||||
for value in self._node.get_values(
|
|
||||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
|
||||||
if self.current_operation is not None:
|
if self.current_operation is not None:
|
||||||
if self._hrt4_zw and self.current_operation == 'Off':
|
if self._hrt4_zw and self.current_operation == 'Off':
|
||||||
# HRT4-ZW can change setpoint when off.
|
# HRT4-ZW can change setpoint when off.
|
||||||
@ -279,17 +295,21 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
|
|
||||||
def set_fan_mode(self, fan):
|
def set_fan_mode(self, fan):
|
||||||
"""Set new target fan mode."""
|
"""Set new target fan mode."""
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE).
|
||||||
if value.command_class == 68 and value.index == 0:
|
values()):
|
||||||
|
if value.command_class == \
|
||||||
|
zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE and \
|
||||||
|
value.index == 0:
|
||||||
value.data = bytes(fan, 'utf-8')
|
value.data = bytes(fan, 'utf-8')
|
||||||
break
|
break
|
||||||
|
|
||||||
def set_operation_mode(self, operation_mode):
|
def set_operation_mode(self, operation_mode):
|
||||||
"""Set new target operation mode."""
|
"""Set new target operation mode."""
|
||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||||
if value.command_class == 64 and value.index == 0:
|
if value.command_class == \
|
||||||
|
zwave.const.COMMAND_CLASS_THERMOSTAT_MODE and value.index == 0:
|
||||||
value.data = bytes(operation_mode, 'utf-8')
|
value.data = bytes(operation_mode, 'utf-8')
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -297,7 +317,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||||||
"""Set new target swing mode."""
|
"""Set new target swing mode."""
|
||||||
if self._zxt_120 == 1:
|
if self._zxt_120 == 1:
|
||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION).values():
|
||||||
if value.command_class == 112 and value.index == 33:
|
if value.command_class == \
|
||||||
|
zwave.const.COMMAND_CLASS_CONFIGURATION and \
|
||||||
|
value.index == 33:
|
||||||
value.data = bytes(swing_mode, 'utf-8')
|
value.data = bytes(swing_mode, 'utf-8')
|
||||||
break
|
break
|
||||||
|
@ -29,6 +29,10 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
|
|||||||
vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower),
|
vol.Required(ATTR_TEXT): vol.All(cv.string, vol.Lower),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({}),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Register the process service."""
|
"""Register the process service."""
|
||||||
@ -48,8 +52,8 @@ def setup(hass, config):
|
|||||||
|
|
||||||
name, command = match.groups()
|
name, command = match.groups()
|
||||||
entities = {state.entity_id: state.name for state in hass.states.all()}
|
entities = {state.entity_id: state.name for state in hass.states.all()}
|
||||||
entity_ids = fuzzyExtract.extractOne(name, entities,
|
entity_ids = fuzzyExtract.extractOne(
|
||||||
score_cutoff=65)[2]
|
name, entities, score_cutoff=65)[2]
|
||||||
|
|
||||||
if not entity_ids:
|
if not entity_ids:
|
||||||
logger.error(
|
logger.error(
|
||||||
@ -70,6 +74,7 @@ def setup(hass, config):
|
|||||||
logger.error('Got unsupported command %s from text %s',
|
logger.error('Got unsupported command %s from text %s',
|
||||||
command, text)
|
command, text)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_PROCESS, process,
|
hass.services.register(
|
||||||
schema=SERVICE_PROCESS_SCHEMA)
|
DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
103
homeassistant/components/cover/mysensors.py
Normal file
103
homeassistant/components/cover/mysensors.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
"""
|
||||||
|
Support for MySensors covers.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/cover.mysensors/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components import mysensors
|
||||||
|
from homeassistant.components.cover import CoverDevice, ATTR_POSITION
|
||||||
|
from homeassistant.const import STATE_ON, STATE_OFF
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
DEPENDENCIES = []
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the mysensors platform for covers."""
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
for gateway in mysensors.GATEWAYS.values():
|
||||||
|
pres = gateway.const.Presentation
|
||||||
|
set_req = gateway.const.SetReq
|
||||||
|
map_sv_types = {
|
||||||
|
pres.S_COVER: [set_req.V_DIMMER, set_req.V_LIGHT],
|
||||||
|
}
|
||||||
|
if float(gateway.protocol_version) >= 1.5:
|
||||||
|
map_sv_types.update({
|
||||||
|
pres.S_COVER: [set_req.V_PERCENTAGE, set_req.V_STATUS],
|
||||||
|
})
|
||||||
|
devices = {}
|
||||||
|
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||||
|
map_sv_types, devices, add_devices, MySensorsCover))
|
||||||
|
|
||||||
|
|
||||||
|
class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):
|
||||||
|
"""Representation of the value of a MySensors Cover child node."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def assumed_state(self):
|
||||||
|
"""Return True if unable to access real state of entity."""
|
||||||
|
return self.gateway.optimistic
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
"""Return True if cover is closed."""
|
||||||
|
set_req = self.gateway.const.SetReq
|
||||||
|
if set_req.V_DIMMER in self._values:
|
||||||
|
return self._values.get(set_req.V_DIMMER) == 0
|
||||||
|
else:
|
||||||
|
return self._values.get(set_req.V_LIGHT) == STATE_OFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_cover_position(self):
|
||||||
|
"""Return current position of cover.
|
||||||
|
|
||||||
|
None is unknown, 0 is closed, 100 is fully open.
|
||||||
|
"""
|
||||||
|
set_req = self.gateway.const.SetReq
|
||||||
|
return self._values.get(set_req.V_DIMMER)
|
||||||
|
|
||||||
|
def open_cover(self, **kwargs):
|
||||||
|
"""Move the cover up."""
|
||||||
|
set_req = self.gateway.const.SetReq
|
||||||
|
self.gateway.set_child_value(
|
||||||
|
self.node_id, self.child_id, set_req.V_UP, 1)
|
||||||
|
if self.gateway.optimistic:
|
||||||
|
# Optimistically assume that cover has changed state.
|
||||||
|
if set_req.V_DIMMER in self._values:
|
||||||
|
self._values[set_req.V_DIMMER] = 100
|
||||||
|
else:
|
||||||
|
self._values[set_req.V_LIGHT] = STATE_ON
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
def close_cover(self, **kwargs):
|
||||||
|
"""Move the cover down."""
|
||||||
|
set_req = self.gateway.const.SetReq
|
||||||
|
self.gateway.set_child_value(
|
||||||
|
self.node_id, self.child_id, set_req.V_DOWN, 1)
|
||||||
|
if self.gateway.optimistic:
|
||||||
|
# Optimistically assume that cover has changed state.
|
||||||
|
if set_req.V_DIMMER in self._values:
|
||||||
|
self._values[set_req.V_DIMMER] = 0
|
||||||
|
else:
|
||||||
|
self._values[set_req.V_LIGHT] = STATE_OFF
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
def set_cover_position(self, **kwargs):
|
||||||
|
"""Move the cover to a specific position."""
|
||||||
|
position = kwargs.get(ATTR_POSITION)
|
||||||
|
set_req = self.gateway.const.SetReq
|
||||||
|
self.gateway.set_child_value(
|
||||||
|
self.node_id, self.child_id, set_req.V_DIMMER, position)
|
||||||
|
if self.gateway.optimistic:
|
||||||
|
# Optimistically assume that cover has changed state.
|
||||||
|
self._values[set_req.V_DIMMER] = position
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
def stop_cover(self, **kwargs):
|
||||||
|
"""Stop the device."""
|
||||||
|
set_req = self.gateway.const.SetReq
|
||||||
|
self.gateway.set_child_value(
|
||||||
|
self.node_id, self.child_id, set_req.V_STOP, 1)
|
@ -12,9 +12,6 @@ from homeassistant.components.zwave import ZWaveDeviceEntity
|
|||||||
from homeassistant.components import zwave
|
from homeassistant.components import zwave
|
||||||
from homeassistant.components.cover import CoverDevice
|
from homeassistant.components.cover import CoverDevice
|
||||||
|
|
||||||
COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38
|
|
||||||
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
|
|
||||||
|
|
||||||
SOMFY = 0x47
|
SOMFY = 0x47
|
||||||
SOMFY_ZRTSI = 0x5a52
|
SOMFY_ZRTSI = 0x5a52
|
||||||
SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI)
|
SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI)
|
||||||
@ -32,17 +29,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None or zwave.NETWORK is None:
|
if discovery_info is None or zwave.NETWORK is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||||
|
|
||||||
if (value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL and
|
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL) \
|
||||||
value.index == 0):
|
and value.index == 0:
|
||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
add_devices([ZwaveRollershutter(value)])
|
add_devices([ZwaveRollershutter(value)])
|
||||||
elif (value.command_class == zwave.COMMAND_CLASS_SWITCH_BINARY or
|
elif node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_BINARY) or \
|
||||||
value.command_class == zwave.COMMAND_CLASS_BARRIER_OPERATOR):
|
node.has_command_class(zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||||
if value.type != zwave.TYPE_BOOL and \
|
if value.type != zwave.const.TYPE_BOOL and \
|
||||||
value.genre != zwave.GENRE_USER:
|
value.genre != zwave.const.GENRE_USER:
|
||||||
return
|
return
|
||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
add_devices([ZwaveGarageDoor(value)])
|
add_devices([ZwaveGarageDoor(value)])
|
||||||
@ -59,6 +56,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||||||
from openzwave.network import ZWaveNetwork
|
from openzwave.network import ZWaveNetwork
|
||||||
from pydispatch import dispatcher
|
from pydispatch import dispatcher
|
||||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||||
|
# pylint: disable=no-member
|
||||||
self._lozwmgr = libopenzwave.PyManager()
|
self._lozwmgr = libopenzwave.PyManager()
|
||||||
self._lozwmgr.create()
|
self._lozwmgr.create()
|
||||||
self._node = value.node
|
self._node = value.node
|
||||||
@ -88,9 +86,10 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||||||
"""Callback on data change for the registered node/value pair."""
|
"""Callback on data change for the registered node/value pair."""
|
||||||
# Position value
|
# Position value
|
||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
if value.command_class == \
|
||||||
and value.label == 'Level':
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and \
|
||||||
|
value.label == 'Level':
|
||||||
self._current_position = value.data
|
self._current_position = value.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -118,22 +117,24 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||||||
def open_cover(self, **kwargs):
|
def open_cover(self, **kwargs):
|
||||||
"""Move the roller shutter up."""
|
"""Move the roller shutter up."""
|
||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
if value.command_class == \
|
||||||
and value.label == 'Open' or \
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
'Open' or value.command_class == \
|
||||||
and value.label == 'Down':
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
|
'Down':
|
||||||
self._lozwmgr.pressButton(value.value_id)
|
self._lozwmgr.pressButton(value.value_id)
|
||||||
break
|
break
|
||||||
|
|
||||||
def close_cover(self, **kwargs):
|
def close_cover(self, **kwargs):
|
||||||
"""Move the roller shutter down."""
|
"""Move the roller shutter down."""
|
||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
if value.command_class == \
|
||||||
and value.label == 'Up' or \
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
'Up' or value.command_class == \
|
||||||
and value.label == 'Close':
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
|
'Close':
|
||||||
self._lozwmgr.pressButton(value.value_id)
|
self._lozwmgr.pressButton(value.value_id)
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -144,11 +145,12 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||||||
def stop_cover(self, **kwargs):
|
def stop_cover(self, **kwargs):
|
||||||
"""Stop the roller shutter."""
|
"""Stop the roller shutter."""
|
||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
if value.command_class == \
|
||||||
and value.label == 'Open' or \
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
'Open' or value.command_class == \
|
||||||
and value.label == 'Down':
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
|
'Down':
|
||||||
self._lozwmgr.releaseButton(value.value_id)
|
self._lozwmgr.releaseButton(value.value_id)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -46,7 +46,6 @@ CONF_TRACK_NEW = 'track_new_devices'
|
|||||||
DEFAULT_TRACK_NEW = True
|
DEFAULT_TRACK_NEW = True
|
||||||
|
|
||||||
CONF_CONSIDER_HOME = 'consider_home'
|
CONF_CONSIDER_HOME = 'consider_home'
|
||||||
DEFAULT_CONSIDER_HOME = 180 # seconds
|
|
||||||
|
|
||||||
CONF_SCAN_INTERVAL = 'interval_seconds'
|
CONF_SCAN_INTERVAL = 'interval_seconds'
|
||||||
DEFAULT_SCAN_INTERVAL = 12
|
DEFAULT_SCAN_INTERVAL = 12
|
||||||
@ -70,8 +69,10 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
|||||||
|
|
||||||
_CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [
|
_CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [
|
||||||
vol.Schema({
|
vol.Schema({
|
||||||
vol.Optional(CONF_TRACK_NEW): cv.boolean,
|
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
|
||||||
vol.Optional(CONF_CONSIDER_HOME): cv.positive_int # seconds
|
vol.Optional(
|
||||||
|
CONF_CONSIDER_HOME, default=timedelta(seconds=180)): vol.All(
|
||||||
|
cv.time_period, cv.positive_timedelta)
|
||||||
}, extra=vol.ALLOW_EXTRA)])}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)])}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
DISCOVERY_PLATFORMS = {
|
DISCOVERY_PLATFORMS = {
|
||||||
@ -118,9 +119,8 @@ def setup(hass: HomeAssistantType, config: ConfigType):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
conf = conf[0] if len(conf) > 0 else {}
|
conf = conf[0] if len(conf) > 0 else {}
|
||||||
consider_home = timedelta(
|
consider_home = conf[CONF_CONSIDER_HOME]
|
||||||
seconds=conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME))
|
track_new = conf[CONF_TRACK_NEW]
|
||||||
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
|
|
||||||
|
|
||||||
devices = load_config(yaml_path, hass, consider_home)
|
devices = load_config(yaml_path, hass, consider_home)
|
||||||
|
|
||||||
@ -282,7 +282,7 @@ class Device(Entity):
|
|||||||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||||
track: bool, dev_id: str, mac: str, name: str=None,
|
track: bool, dev_id: str, mac: str, name: str=None,
|
||||||
picture: str=None, gravatar: str=None,
|
picture: str=None, gravatar: str=None,
|
||||||
away_hide: bool=False) -> None:
|
hide_if_away: bool=False) -> None:
|
||||||
"""Initialize a device."""
|
"""Initialize a device."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||||
@ -307,7 +307,7 @@ class Device(Entity):
|
|||||||
else:
|
else:
|
||||||
self.config_picture = picture
|
self.config_picture = picture
|
||||||
|
|
||||||
self.away_hide = away_hide
|
self.away_hide = hide_if_away
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -398,15 +398,29 @@ class Device(Entity):
|
|||||||
|
|
||||||
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||||
"""Load devices from YAML configuration file."""
|
"""Load devices from YAML configuration file."""
|
||||||
|
dev_schema = vol.Schema({
|
||||||
|
vol.Required('name'): cv.string,
|
||||||
|
vol.Optional('track', default=False): cv.boolean,
|
||||||
|
vol.Optional('mac', default=None): vol.Any(None, vol.All(cv.string,
|
||||||
|
vol.Upper)),
|
||||||
|
vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean,
|
||||||
|
vol.Optional('gravatar', default=None): vol.Any(None, cv.string),
|
||||||
|
vol.Optional('picture', default=None): vol.Any(None, cv.string),
|
||||||
|
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
|
||||||
|
cv.time_period, cv.positive_timedelta)
|
||||||
|
})
|
||||||
try:
|
try:
|
||||||
return [
|
result = []
|
||||||
Device(hass, consider_home, device.get('track', False),
|
devices = load_yaml_config_file(path)
|
||||||
str(dev_id).lower(), None if device.get('mac') is None
|
for dev_id, device in devices.items():
|
||||||
else str(device.get('mac')).upper(),
|
try:
|
||||||
device.get('name'), device.get('picture'),
|
device = dev_schema(device)
|
||||||
device.get('gravatar'),
|
device['dev_id'] = cv.slug(dev_id)
|
||||||
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
|
except vol.Invalid as exp:
|
||||||
for dev_id, device in load_yaml_config_file(path).items()]
|
log_exception(exp, dev_id, devices)
|
||||||
|
else:
|
||||||
|
result.append(Device(hass, **device))
|
||||||
|
return result
|
||||||
except (HomeAssistantError, FileNotFoundError):
|
except (HomeAssistantError, FileNotFoundError):
|
||||||
# When YAML file could not be loaded/did not contain a dict
|
# When YAML file could not be loaded/did not contain a dict
|
||||||
return []
|
return []
|
||||||
|
@ -85,7 +85,9 @@ class FritzBoxScanner(object):
|
|||||||
|
|
||||||
def get_device_name(self, mac):
|
def get_device_name(self, mac):
|
||||||
"""Return the name of the given device or None if is not known."""
|
"""Return the name of the given device or None if is not known."""
|
||||||
ret = self.fritz_box.get_specific_host_entry(mac)['NewHostName']
|
ret = self.fritz_box.get_specific_host_entry(mac).get(
|
||||||
|
'NewHostName'
|
||||||
|
)
|
||||||
if ret == {}:
|
if ret == {}:
|
||||||
return None
|
return None
|
||||||
return ret
|
return ret
|
||||||
|
@ -7,6 +7,7 @@ https://home-assistant.io/components/device_tracker.owntracks/
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
import base64
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -18,53 +19,121 @@ from homeassistant.util import convert, slugify
|
|||||||
from homeassistant.components import zone as zone_comp
|
from homeassistant.components import zone as zone_comp
|
||||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||||
|
|
||||||
DEPENDENCIES = ['mqtt']
|
REQUIREMENTS = ['libnacl==1.5.0']
|
||||||
|
|
||||||
REGIONS_ENTERED = defaultdict(list)
|
|
||||||
MOBILE_BEACONS_ACTIVE = defaultdict(list)
|
|
||||||
|
|
||||||
BEACON_DEV_ID = 'beacon'
|
|
||||||
|
|
||||||
LOCATION_TOPIC = 'owntracks/+/+'
|
|
||||||
EVENT_TOPIC = 'owntracks/+/+/event'
|
|
||||||
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
LOCK = threading.Lock()
|
BEACON_DEV_ID = 'beacon'
|
||||||
|
|
||||||
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
|
CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'
|
||||||
|
CONF_SECRET = 'secret'
|
||||||
CONF_WAYPOINT_IMPORT = 'waypoints'
|
CONF_WAYPOINT_IMPORT = 'waypoints'
|
||||||
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
|
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
|
||||||
|
|
||||||
|
DEPENDENCIES = ['mqtt']
|
||||||
|
|
||||||
|
EVENT_TOPIC = 'owntracks/+/+/event'
|
||||||
|
|
||||||
|
LOCATION_TOPIC = 'owntracks/+/+'
|
||||||
|
LOCK = threading.Lock()
|
||||||
|
|
||||||
|
MOBILE_BEACONS_ACTIVE = defaultdict(list)
|
||||||
|
|
||||||
|
REGIONS_ENTERED = defaultdict(list)
|
||||||
|
|
||||||
VALIDATE_LOCATION = 'location'
|
VALIDATE_LOCATION = 'location'
|
||||||
VALIDATE_TRANSITION = 'transition'
|
VALIDATE_TRANSITION = 'transition'
|
||||||
VALIDATE_WAYPOINTS = 'waypoints'
|
VALIDATE_WAYPOINTS = 'waypoints'
|
||||||
|
|
||||||
WAYPOINT_LAT_KEY = 'lat'
|
WAYPOINT_LAT_KEY = 'lat'
|
||||||
WAYPOINT_LON_KEY = 'lon'
|
WAYPOINT_LON_KEY = 'lon'
|
||||||
|
WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
|
vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float),
|
||||||
vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean,
|
vol.Optional(CONF_WAYPOINT_IMPORT, default=True): cv.boolean,
|
||||||
vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(cv.ensure_list, [cv.string])
|
vol.Optional(CONF_WAYPOINT_WHITELIST): vol.All(
|
||||||
|
cv.ensure_list, [cv.string]),
|
||||||
|
vol.Optional(CONF_SECRET): vol.Any(
|
||||||
|
vol.Schema({vol.Optional(cv.string): cv.string}),
|
||||||
|
cv.string)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def get_cipher():
|
||||||
|
"""Return decryption function and length of key."""
|
||||||
|
from libnacl import crypto_secretbox_KEYBYTES as KEYLEN
|
||||||
|
from libnacl.secret import SecretBox
|
||||||
|
|
||||||
|
def decrypt(ciphertext, key):
|
||||||
|
"""Decrypt ciphertext using key."""
|
||||||
|
return SecretBox(key).decrypt(ciphertext)
|
||||||
|
return (KEYLEN, decrypt)
|
||||||
|
|
||||||
|
|
||||||
def setup_scanner(hass, config, see):
|
def setup_scanner(hass, config, see):
|
||||||
"""Setup an OwnTracks tracker."""
|
"""Set up an OwnTracks tracker."""
|
||||||
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)
|
||||||
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
waypoint_import = config.get(CONF_WAYPOINT_IMPORT)
|
||||||
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
|
waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST)
|
||||||
|
secret = config.get(CONF_SECRET)
|
||||||
|
|
||||||
|
def decrypt_payload(topic, ciphertext):
|
||||||
|
"""Decrypt encrypted payload."""
|
||||||
|
try:
|
||||||
|
keylen, decrypt = get_cipher()
|
||||||
|
except OSError:
|
||||||
|
_LOGGER.warning('Ignoring encrypted payload '
|
||||||
|
'because libsodium not installed.')
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(secret, dict):
|
||||||
|
key = secret.get(topic)
|
||||||
|
else:
|
||||||
|
key = secret
|
||||||
|
|
||||||
|
if key is None:
|
||||||
|
_LOGGER.warning('Ignoring encrypted payload '
|
||||||
|
'because no decryption key known '
|
||||||
|
'for topic %s.', topic)
|
||||||
|
return None
|
||||||
|
|
||||||
|
key = key.encode("utf-8")
|
||||||
|
key = key[:keylen]
|
||||||
|
key = key.ljust(keylen, b'\0')
|
||||||
|
|
||||||
|
try:
|
||||||
|
ciphertext = base64.b64decode(ciphertext)
|
||||||
|
message = decrypt(ciphertext, key)
|
||||||
|
message = message.decode("utf-8")
|
||||||
|
_LOGGER.debug("Decrypted payload: %s", message)
|
||||||
|
return message
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning('Ignoring encrypted payload '
|
||||||
|
'because unable to decrypt using key '
|
||||||
|
'for topic %s.', topic)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def validate_payload(topic, payload, data_type):
|
||||||
|
"""Validate the OwnTracks payload."""
|
||||||
|
# pylint: disable=too-many-return-statements
|
||||||
|
|
||||||
def validate_payload(payload, data_type):
|
|
||||||
"""Validate OwnTracks payload."""
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(payload)
|
data = json.loads(payload)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# If invalid JSON
|
# If invalid JSON
|
||||||
_LOGGER.error('Unable to parse payload as JSON: %s', payload)
|
_LOGGER.error('Unable to parse payload as JSON: %s', payload)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if isinstance(data, dict) and \
|
||||||
|
data.get('_type') == 'encrypted' and \
|
||||||
|
'data' in data:
|
||||||
|
plaintext_payload = decrypt_payload(topic, data['data'])
|
||||||
|
if plaintext_payload is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return validate_payload(topic, plaintext_payload, data_type)
|
||||||
|
|
||||||
if not isinstance(data, dict) or data.get('_type') != data_type:
|
if not isinstance(data, dict) or data.get('_type') != data_type:
|
||||||
_LOGGER.debug('Skipping %s update for following data '
|
_LOGGER.debug('Skipping %s update for following data '
|
||||||
'because of missing or malformatted data: %s',
|
'because of missing or malformatted data: %s',
|
||||||
@ -90,7 +159,7 @@ def setup_scanner(hass, config, see):
|
|||||||
"""MQTT message received."""
|
"""MQTT message received."""
|
||||||
# Docs on available data:
|
# Docs on available data:
|
||||||
# http://owntracks.org/booklet/tech/json/#_typelocation
|
# http://owntracks.org/booklet/tech/json/#_typelocation
|
||||||
data = validate_payload(payload, VALIDATE_LOCATION)
|
data = validate_payload(topic, payload, VALIDATE_LOCATION)
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -111,7 +180,7 @@ def setup_scanner(hass, config, see):
|
|||||||
"""MQTT event (geofences) received."""
|
"""MQTT event (geofences) received."""
|
||||||
# Docs on available data:
|
# Docs on available data:
|
||||||
# http://owntracks.org/booklet/tech/json/#_typetransition
|
# http://owntracks.org/booklet/tech/json/#_typetransition
|
||||||
data = validate_payload(payload, VALIDATE_TRANSITION)
|
data = validate_payload(topic, payload, VALIDATE_TRANSITION)
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -206,7 +275,7 @@ def setup_scanner(hass, config, see):
|
|||||||
"""List of waypoints published by a user."""
|
"""List of waypoints published by a user."""
|
||||||
# Docs on available data:
|
# Docs on available data:
|
||||||
# http://owntracks.org/booklet/tech/json/#_typewaypoints
|
# http://owntracks.org/booklet/tech/json/#_typewaypoints
|
||||||
data = validate_payload(payload, VALIDATE_WAYPOINTS)
|
data = validate_payload(topic, payload, VALIDATE_WAYPOINTS)
|
||||||
if not data:
|
if not data:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
100
homeassistant/components/device_tracker/volvooncall.py
Normal file
100
homeassistant/components/device_tracker/volvooncall.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
"""
|
||||||
|
Support for Volvo On Call.
|
||||||
|
|
||||||
|
http://www.volvocars.com/intl/own/owner-info/volvo-on-call
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/device_tracker.volvooncall/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
import voluptuous as vol
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.event import track_point_in_utc_time
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
|
CONF_USERNAME)
|
||||||
|
from homeassistant.components.device_tracker import (
|
||||||
|
DEFAULT_SCAN_INTERVAL,
|
||||||
|
PLATFORM_SCHEMA)
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SERVICE_URL = 'https://vocapi.wirelesscar.net/customerapi/rest/v3.0/'
|
||||||
|
HEADERS = {"X-Device-Id": "Device",
|
||||||
|
"X-OS-Type": "Android",
|
||||||
|
"X-Originator-Type": "App"}
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_scanner(hass, config, see):
|
||||||
|
"""Validate the configuration and return a scanner."""
|
||||||
|
session = requests.Session()
|
||||||
|
session.headers.update(HEADERS)
|
||||||
|
session.auth = (config.get(CONF_USERNAME),
|
||||||
|
config.get(CONF_PASSWORD))
|
||||||
|
|
||||||
|
interval = max(MIN_TIME_BETWEEN_SCANS.seconds,
|
||||||
|
config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))
|
||||||
|
|
||||||
|
def query(ref, rel=SERVICE_URL):
|
||||||
|
"""Perform a query to the online service."""
|
||||||
|
url = urljoin(rel, ref)
|
||||||
|
_LOGGER.debug("Request for %s", url)
|
||||||
|
res = session.get(url, timeout=15)
|
||||||
|
res.raise_for_status()
|
||||||
|
_LOGGER.debug("Received %s", res.json())
|
||||||
|
return res.json()
|
||||||
|
|
||||||
|
def update(now):
|
||||||
|
"""Update status from the online service."""
|
||||||
|
try:
|
||||||
|
_LOGGER.debug("Updating")
|
||||||
|
status = query("status", vehicle_url)
|
||||||
|
position = query("position", vehicle_url)
|
||||||
|
see(dev_id=dev_id,
|
||||||
|
host_name=host_name,
|
||||||
|
gps=(position["position"]["latitude"],
|
||||||
|
position["position"]["longitude"]),
|
||||||
|
attributes=dict(
|
||||||
|
tank_volume=attributes["fuelTankVolume"],
|
||||||
|
washer_fluid=status["washerFluidLevel"],
|
||||||
|
brake_fluid=status["brakeFluid"],
|
||||||
|
service_warning=status["serviceWarningStatus"],
|
||||||
|
fuel=status["fuelAmount"],
|
||||||
|
odometer=status["odometer"],
|
||||||
|
range=status["distanceToEmpty"]))
|
||||||
|
except requests.exceptions.RequestException as error:
|
||||||
|
_LOGGER.error("Could not query server: %s", error)
|
||||||
|
finally:
|
||||||
|
track_point_in_utc_time(hass, update,
|
||||||
|
now + timedelta(seconds=interval))
|
||||||
|
|
||||||
|
try:
|
||||||
|
_LOGGER.info('Logging in to service')
|
||||||
|
user = query("customeraccounts")
|
||||||
|
rel = query(user["accountVehicleRelations"][0])
|
||||||
|
vehicle_url = rel["vehicle"] + '/'
|
||||||
|
attributes = query("attributes", vehicle_url)
|
||||||
|
|
||||||
|
dev_id = "volvo_" + attributes["registrationNumber"]
|
||||||
|
host_name = "%s %s/%s" % (attributes["registrationNumber"],
|
||||||
|
attributes["vehicleType"],
|
||||||
|
attributes["modelYear"])
|
||||||
|
update(utcnow())
|
||||||
|
return True
|
||||||
|
except requests.exceptions.RequestException as error:
|
||||||
|
_LOGGER.error("Could not log in to service. "
|
||||||
|
"Please check configuration: "
|
||||||
|
"%s", error)
|
||||||
|
return False
|
86
homeassistant/components/digital_ocean.py
Normal file
86
homeassistant/components/digital_ocean.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
"""
|
||||||
|
Support for Digital Ocean.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/digital_ocean/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
REQUIREMENTS = ['python-digitalocean==1.9.0']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ATTR_CREATED_AT = 'created_at'
|
||||||
|
ATTR_DROPLET_ID = 'droplet_id'
|
||||||
|
ATTR_DROPLET_NAME = 'droplet_name'
|
||||||
|
ATTR_FEATURES = 'features'
|
||||||
|
ATTR_IPV4_ADDRESS = 'ipv4_address'
|
||||||
|
ATTR_IPV6_ADDRESS = 'ipv6_address'
|
||||||
|
ATTR_MEMORY = 'memory'
|
||||||
|
ATTR_REGION = 'region'
|
||||||
|
ATTR_VCPUS = 'vcpus'
|
||||||
|
|
||||||
|
CONF_DROPLETS = 'droplets'
|
||||||
|
|
||||||
|
DIGITAL_OCEAN = None
|
||||||
|
DIGITAL_OCEAN_PLATFORMS = ['switch', 'binary_sensor']
|
||||||
|
DOMAIN = 'digital_ocean'
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Required(CONF_ACCESS_TOKEN): cv.string,
|
||||||
|
}),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument,too-few-public-methods
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Setup the Digital Ocean component."""
|
||||||
|
conf = config[DOMAIN]
|
||||||
|
access_token = conf.get(CONF_ACCESS_TOKEN)
|
||||||
|
|
||||||
|
global DIGITAL_OCEAN
|
||||||
|
DIGITAL_OCEAN = DigitalOcean(access_token)
|
||||||
|
|
||||||
|
if not DIGITAL_OCEAN.manager.get_account():
|
||||||
|
_LOGGER.error("No Digital Ocean account found for the given API Token")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class DigitalOcean(object):
|
||||||
|
"""Handle all communication with the Digital Ocean API."""
|
||||||
|
|
||||||
|
def __init__(self, access_token):
|
||||||
|
"""Initialize the Digital Ocean connection."""
|
||||||
|
import digitalocean
|
||||||
|
|
||||||
|
self._access_token = access_token
|
||||||
|
self.data = None
|
||||||
|
self.manager = digitalocean.Manager(token=self._access_token)
|
||||||
|
|
||||||
|
def get_droplet_id(self, droplet_name):
|
||||||
|
"""Get the status of a Digital Ocean droplet."""
|
||||||
|
droplet_id = None
|
||||||
|
|
||||||
|
all_droplets = self.manager.get_all_droplets()
|
||||||
|
for droplet in all_droplets:
|
||||||
|
if droplet_name == droplet.name:
|
||||||
|
droplet_id = droplet.id
|
||||||
|
|
||||||
|
return droplet_id
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
def update(self):
|
||||||
|
"""Use the data from Digital Ocean API."""
|
||||||
|
self.data = self.manager.get_all_droplets()
|
@ -9,6 +9,8 @@ loaded before the EVENT_PLATFORM_DISCOVERED is fired.
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||||
from homeassistant.helpers.discovery import load_platform, discover
|
from homeassistant.helpers.discovery import load_platform, discover
|
||||||
|
|
||||||
@ -33,6 +35,10 @@ SERVICE_HANDLERS = {
|
|||||||
'directv': ('media_player', 'directv'),
|
'directv': ('media_player', 'directv'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({}),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Start a discovery service."""
|
"""Start a discovery service."""
|
||||||
|
@ -25,6 +25,8 @@ from homeassistant.components.light import (
|
|||||||
from homeassistant.components.http import (
|
from homeassistant.components.http import (
|
||||||
HomeAssistantView, HomeAssistantWSGI
|
HomeAssistantView, HomeAssistantWSGI
|
||||||
)
|
)
|
||||||
|
# pylint: disable=unused-import
|
||||||
|
from homeassistant.components.http import REQUIREMENTS # noqa
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DOMAIN = 'emulated_hue'
|
DOMAIN = 'emulated_hue'
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||||
|
|
||||||
FINGERPRINTS = {
|
FINGERPRINTS = {
|
||||||
"core.js": "78862c0984279b6876f594ffde45177c",
|
"core.js": "9b3e5ab4eac7e3b074e0daf3f619a638",
|
||||||
"frontend.html": "c1753e1ce530f978036742477c96d2fd",
|
"frontend.html": "5854807d361de26fe93ad474010f19d2",
|
||||||
"mdi.html": "6bd013a8252e19b3c1f1de52994cfbe4",
|
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
||||||
"panels/ha-panel-dev-event.html": "c4a5f70eece9f92616a65e8d26be803e",
|
"panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002",
|
||||||
"panels/ha-panel-dev-info.html": "34e2df1af32e60fffcafe7e008a92169",
|
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
|
||||||
"panels/ha-panel-dev-service.html": "07e83c6b7f79d78a59258f6dba477b54",
|
"panels/ha-panel-dev-service.html": "c7974458ebc33412d95497e99b785e12",
|
||||||
"panels/ha-panel-dev-state.html": "fd8eb946856b1346a87a51d0c86854ff",
|
"panels/ha-panel-dev-state.html": "4be627b74e683af14ef779d8203ec674",
|
||||||
"panels/ha-panel-dev-template.html": "7cff8a2ef3f44fdaf357a0d41696bf6d",
|
"panels/ha-panel-dev-template.html": "d23943fa0370f168714da407c90091a2",
|
||||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||||
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
|
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
|
||||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1 +1 @@
|
|||||||
Subproject commit 0a4454c68f3c29c77cd60f4315d410d8b3737543
|
Subproject commit db109f5dda043182a7e9647b161851e83be9b91e
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -1,2 +1,2 @@
|
|||||||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:24px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1)
|
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:24px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1)
|
||||||
clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/balloob/home-assistant" target="_blank">server</a> — <a href="https://github.com/balloob/home-assistant-polymer" target="_blank">frontend-ui</a> — <a href="https://github.com/balloob/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>
|
clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p>Path to configuration.yaml: [[hassConfigDir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a> — <a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a> — <a href="https://github.com/home-assistant/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},hassConfigDir:{type:String,bindNuclear:function(r){return r.configGetters.configDir}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -22,15 +22,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None or zwave.NETWORK is None:
|
if discovery_info is None or zwave.NETWORK is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||||
|
|
||||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY and \
|
if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_BINARY and \
|
||||||
value.command_class != zwave.COMMAND_CLASS_BARRIER_OPERATOR:
|
value.command_class != zwave.const.COMMAND_CLASS_BARRIER_OPERATOR:
|
||||||
return
|
return
|
||||||
if value.type != zwave.TYPE_BOOL:
|
if value.type != zwave.const.TYPE_BOOL:
|
||||||
return
|
return
|
||||||
if value.genre != zwave.GENRE_USER:
|
if value.genre != zwave.const.GENRE_USER:
|
||||||
return
|
return
|
||||||
|
|
||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
|
@ -23,7 +23,7 @@ from homeassistant.config import load_yaml_config_file
|
|||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
DOMAIN = 'homematic'
|
DOMAIN = 'homematic'
|
||||||
REQUIREMENTS = ["pyhomematic==0.1.14"]
|
REQUIREMENTS = ["pyhomematic==0.1.16"]
|
||||||
|
|
||||||
HOMEMATIC = None
|
HOMEMATIC = None
|
||||||
HOMEMATIC_LINK_DELAY = 0.5
|
HOMEMATIC_LINK_DELAY = 0.5
|
||||||
@ -52,17 +52,22 @@ SERVICE_VIRTUALKEY = 'virtualkey'
|
|||||||
SERVICE_SET_VALUE = 'set_value'
|
SERVICE_SET_VALUE = 'set_value'
|
||||||
|
|
||||||
HM_DEVICE_TYPES = {
|
HM_DEVICE_TYPES = {
|
||||||
DISCOVER_SWITCHES: ['Switch', 'SwitchPowermeter'],
|
DISCOVER_SWITCHES: [
|
||||||
DISCOVER_LIGHTS: ['Dimmer'],
|
'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch',
|
||||||
DISCOVER_SENSORS: ['SwitchPowermeter', 'Motion', 'MotionV2',
|
'IPSwitchPowermeter', 'KeyMatic'],
|
||||||
'RemoteMotion', 'ThermostatWall', 'AreaThermostat',
|
DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer'],
|
||||||
'RotaryHandleSensor', 'WaterSensor', 'PowermeterGas',
|
DISCOVER_SENSORS: [
|
||||||
'LuxSensor', 'WeatherSensor', 'WeatherStation'],
|
'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion',
|
||||||
DISCOVER_CLIMATE: ['Thermostat', 'ThermostatWall', 'MAXThermostat'],
|
'ThermostatWall', 'AreaThermostat', 'RotaryHandleSensor',
|
||||||
DISCOVER_BINARY_SENSORS: ['ShutterContact', 'Smoke', 'SmokeV2', 'Motion',
|
'WaterSensor', 'PowermeterGas', 'LuxSensor', 'WeatherSensor',
|
||||||
'MotionV2', 'RemoteMotion', 'WeatherSensor',
|
'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor',
|
||||||
'TiltSensor'],
|
'TemperatureSensor', 'CO2Sensor'],
|
||||||
DISCOVER_COVER: ['Blind']
|
DISCOVER_CLIMATE: [
|
||||||
|
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2'],
|
||||||
|
DISCOVER_BINARY_SENSORS: [
|
||||||
|
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
|
||||||
|
'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact'],
|
||||||
|
DISCOVER_COVER: ['Blind', 'KeyBlind']
|
||||||
}
|
}
|
||||||
|
|
||||||
HM_IGNORE_DISCOVERY_NODE = [
|
HM_IGNORE_DISCOVERY_NODE = [
|
||||||
@ -87,11 +92,12 @@ HM_PRESS_EVENTS = [
|
|||||||
'PRESS_SHORT',
|
'PRESS_SHORT',
|
||||||
'PRESS_LONG',
|
'PRESS_LONG',
|
||||||
'PRESS_CONT',
|
'PRESS_CONT',
|
||||||
'PRESS_LONG_RELEASE'
|
'PRESS_LONG_RELEASE',
|
||||||
|
'PRESS',
|
||||||
]
|
]
|
||||||
|
|
||||||
HM_IMPULSE_EVENTS = [
|
HM_IMPULSE_EVENTS = [
|
||||||
'SEQUENCE_OK'
|
'SEQUENCE_OK',
|
||||||
]
|
]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -111,6 +117,15 @@ CONF_RESOLVENAMES = 'resolvenames'
|
|||||||
CONF_DELAY = 'delay'
|
CONF_DELAY = 'delay'
|
||||||
CONF_VARIABLES = 'variables'
|
CONF_VARIABLES = 'variables'
|
||||||
|
|
||||||
|
DEFAULT_LOCAL_IP = "0.0.0.0"
|
||||||
|
DEFAULT_LOCAL_PORT = 0
|
||||||
|
DEFAULT_RESOLVENAMES = False
|
||||||
|
DEFAULT_REMOTE_PORT = 2001
|
||||||
|
DEFAULT_USERNAME = "Admin"
|
||||||
|
DEFAULT_PASSWORD = ""
|
||||||
|
DEFAULT_VARIABLES = False
|
||||||
|
DEFAULT_DELAY = 0.5
|
||||||
|
|
||||||
|
|
||||||
DEVICE_SCHEMA = vol.Schema({
|
DEVICE_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_PLATFORM): "homematic",
|
vol.Required(CONF_PLATFORM): "homematic",
|
||||||
@ -122,16 +137,16 @@ DEVICE_SCHEMA = vol.Schema({
|
|||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
vol.Required(CONF_LOCAL_IP): cv.string,
|
|
||||||
vol.Optional(CONF_LOCAL_PORT, default=8943): cv.port,
|
|
||||||
vol.Required(CONF_REMOTE_IP): cv.string,
|
vol.Required(CONF_REMOTE_IP): cv.string,
|
||||||
vol.Optional(CONF_REMOTE_PORT, default=2001): cv.port,
|
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
|
||||||
vol.Optional(CONF_RESOLVENAMES, default=False):
|
vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port,
|
||||||
|
vol.Optional(CONF_REMOTE_PORT, default=DEFAULT_REMOTE_PORT): cv.port,
|
||||||
|
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
|
||||||
vol.In(CONF_RESOLVENAMES_OPTIONS),
|
vol.In(CONF_RESOLVENAMES_OPTIONS),
|
||||||
vol.Optional(CONF_USERNAME, default="Admin"): cv.string,
|
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_PASSWORD, default=""): cv.string,
|
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_DELAY, default=0.5): vol.Coerce(float),
|
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): vol.Coerce(float),
|
||||||
vol.Optional(CONF_VARIABLES, default=False): cv.boolean,
|
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES): cv.boolean,
|
||||||
}),
|
}),
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
@ -323,83 +338,45 @@ def _get_devices(device_type, keys):
|
|||||||
metadata.update(device.SENSORNODE)
|
metadata.update(device.SENSORNODE)
|
||||||
elif device_type == DISCOVER_BINARY_SENSORS:
|
elif device_type == DISCOVER_BINARY_SENSORS:
|
||||||
metadata.update(device.BINARYNODE)
|
metadata.update(device.BINARYNODE)
|
||||||
|
else:
|
||||||
|
metadata.update({None: device.ELEMENT})
|
||||||
|
|
||||||
params = _create_params_list(device, metadata, device_type)
|
if metadata:
|
||||||
if params:
|
|
||||||
# Generate options for 1...n elements with 1...n params
|
# Generate options for 1...n elements with 1...n params
|
||||||
for channel in range(1, device.ELEMENT + 1):
|
for param, channels in metadata.items():
|
||||||
_LOGGER.debug("Handling %s:%i", key, channel)
|
if param in HM_IGNORE_DISCOVERY_NODE:
|
||||||
if channel in params:
|
continue
|
||||||
for param in params[channel]:
|
|
||||||
name = _create_ha_name(
|
|
||||||
name=device.NAME,
|
|
||||||
channel=channel,
|
|
||||||
param=param
|
|
||||||
)
|
|
||||||
device_dict = {
|
|
||||||
CONF_PLATFORM: "homematic",
|
|
||||||
ATTR_ADDRESS: key,
|
|
||||||
ATTR_NAME: name,
|
|
||||||
ATTR_CHANNEL: channel
|
|
||||||
}
|
|
||||||
if param is not None:
|
|
||||||
device_dict.update({ATTR_PARAM: param})
|
|
||||||
|
|
||||||
# Add new device
|
# add devices
|
||||||
try:
|
_LOGGER.debug("Handling %s: %s", param, channels)
|
||||||
DEVICE_SCHEMA(device_dict)
|
for channel in channels:
|
||||||
device_arr.append(device_dict)
|
name = _create_ha_name(
|
||||||
except vol.MultipleInvalid as err:
|
name=device.NAME,
|
||||||
_LOGGER.error("Invalid device config: %s",
|
channel=channel,
|
||||||
str(err))
|
param=param
|
||||||
else:
|
)
|
||||||
_LOGGER.debug("Channel %i not in params", channel)
|
device_dict = {
|
||||||
|
CONF_PLATFORM: "homematic",
|
||||||
|
ATTR_ADDRESS: key,
|
||||||
|
ATTR_NAME: name,
|
||||||
|
ATTR_CHANNEL: channel
|
||||||
|
}
|
||||||
|
if param is not None:
|
||||||
|
device_dict[ATTR_PARAM] = param
|
||||||
|
|
||||||
|
# Add new device
|
||||||
|
try:
|
||||||
|
DEVICE_SCHEMA(device_dict)
|
||||||
|
device_arr.append(device_dict)
|
||||||
|
except vol.MultipleInvalid as err:
|
||||||
|
_LOGGER.error("Invalid device config: %s",
|
||||||
|
str(err))
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug("Got no params for %s", key)
|
_LOGGER.debug("Got no params for %s", key)
|
||||||
_LOGGER.debug("%s autodiscovery: %s", device_type, str(device_arr))
|
_LOGGER.debug("%s autodiscovery: %s", device_type, str(device_arr))
|
||||||
return device_arr
|
return device_arr
|
||||||
|
|
||||||
|
|
||||||
def _create_params_list(hmdevice, metadata, device_type):
|
|
||||||
"""Create a list from HMDevice with all possible parameters in config."""
|
|
||||||
params = {}
|
|
||||||
merge = False
|
|
||||||
|
|
||||||
# use merge?
|
|
||||||
if device_type in (DISCOVER_SENSORS, DISCOVER_BINARY_SENSORS):
|
|
||||||
merge = True
|
|
||||||
|
|
||||||
# Search in sensor and binary metadata per elements
|
|
||||||
for channel in range(1, hmdevice.ELEMENT + 1):
|
|
||||||
param_chan = []
|
|
||||||
for node, meta_chan in metadata.items():
|
|
||||||
try:
|
|
||||||
# Is this attribute ignored?
|
|
||||||
if node in HM_IGNORE_DISCOVERY_NODE:
|
|
||||||
continue
|
|
||||||
if meta_chan == 'c' or meta_chan is None:
|
|
||||||
# Only channel linked data
|
|
||||||
param_chan.append(node)
|
|
||||||
elif channel == 1:
|
|
||||||
# First channel can have other data channel
|
|
||||||
param_chan.append(node)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
_LOGGER.error("Exception generating %s (%s)",
|
|
||||||
hmdevice.ADDRESS, str(metadata))
|
|
||||||
|
|
||||||
# default parameter is merge is off
|
|
||||||
if len(param_chan) == 0 and not merge:
|
|
||||||
param_chan.append(None)
|
|
||||||
|
|
||||||
# Add to channel
|
|
||||||
if len(param_chan) > 0:
|
|
||||||
params.update({channel: param_chan})
|
|
||||||
|
|
||||||
_LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS,
|
|
||||||
str(params))
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def _create_ha_name(name, channel, param):
|
def _create_ha_name(name, channel, param):
|
||||||
"""Generate a unique object name."""
|
"""Generate a unique object name."""
|
||||||
# HMDevice is a simple device
|
# HMDevice is a simple device
|
||||||
@ -484,12 +461,12 @@ def _hm_service_virtualkey(call):
|
|||||||
hmdevice = HOMEMATIC.devices.get(address)
|
hmdevice = HOMEMATIC.devices.get(address)
|
||||||
|
|
||||||
# if param exists for this device
|
# if param exists for this device
|
||||||
if param not in hmdevice.ACTIONNODE:
|
if hmdevice is None or param not in hmdevice.ACTIONNODE:
|
||||||
_LOGGER.error("%s not datapoint in hm device %s", param, address)
|
_LOGGER.error("%s not datapoint in hm device %s", param, address)
|
||||||
return
|
return
|
||||||
|
|
||||||
# channel exists?
|
# channel exists?
|
||||||
if channel > hmdevice.ELEMENT:
|
if channel in hmdevice.ACTIONNODE[param]:
|
||||||
_LOGGER.error("%i is not a channel in hm device %s", channel, address)
|
_LOGGER.error("%i is not a channel in hm device %s", channel, address)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -743,19 +720,22 @@ class HMDevice(Entity):
|
|||||||
self._hmdevice.ATTRIBUTENODE,
|
self._hmdevice.ATTRIBUTENODE,
|
||||||
self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE,
|
self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE,
|
||||||
self._hmdevice.ACTIONNODE):
|
self._hmdevice.ACTIONNODE):
|
||||||
for node, channel in metadata.items():
|
for node, channels in metadata.items():
|
||||||
# Data is needed for this instance
|
# Data is needed for this instance
|
||||||
if node in self._data:
|
if node in self._data:
|
||||||
# chan is current channel
|
# chan is current channel
|
||||||
if channel == 'c' or channel is None:
|
if len(channels) == 1:
|
||||||
|
channel = channels[0]
|
||||||
|
else:
|
||||||
channel = self._channel
|
channel = self._channel
|
||||||
|
|
||||||
# Prepare for subscription
|
# Prepare for subscription
|
||||||
try:
|
try:
|
||||||
if int(channel) >= 0:
|
if int(channel) >= 0:
|
||||||
channels_to_sub.update({int(channel): True})
|
channels_to_sub.update({int(channel): True})
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
_LOGGER("Invalid channel in metadata from %s",
|
_LOGGER.error("Invalid channel in metadata from %s",
|
||||||
self._name)
|
self._name)
|
||||||
|
|
||||||
# Set callbacks
|
# Set callbacks
|
||||||
for channel in channels_to_sub:
|
for channel in channels_to_sub:
|
||||||
|
@ -11,18 +11,20 @@ import mimetypes
|
|||||||
import threading
|
import threading
|
||||||
import re
|
import re
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.remote as rem
|
import homeassistant.remote as rem
|
||||||
from homeassistant import util
|
from homeassistant import util
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL,
|
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL,
|
||||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE_JSON,
|
||||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS,
|
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS,
|
||||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||||
from homeassistant.core import split_entity_id
|
from homeassistant.core import split_entity_id
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components import persistent_notification
|
||||||
|
|
||||||
DOMAIN = 'http'
|
DOMAIN = 'http'
|
||||||
REQUIREMENTS = ('cherrypy==8.1.0', 'static3==0.7.0', 'Werkzeug==0.11.11')
|
REQUIREMENTS = ('cherrypy==8.1.0', 'static3==0.7.0', 'Werkzeug==0.11.11')
|
||||||
@ -37,6 +39,7 @@ CONF_SSL_KEY = 'ssl_key'
|
|||||||
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
||||||
|
|
||||||
DATA_API_PASSWORD = 'api_password'
|
DATA_API_PASSWORD = 'api_password'
|
||||||
|
NOTIFICATION_ID_LOGIN = 'http-login'
|
||||||
|
|
||||||
# TLS configuation follows the best-practice guidelines specified here:
|
# TLS configuation follows the best-practice guidelines specified here:
|
||||||
# https://wiki.mozilla.org/Security/Server_Side_TLS
|
# https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||||
@ -106,7 +109,7 @@ def setup(hass, config):
|
|||||||
api_password = util.convert(conf.get(CONF_API_PASSWORD), str)
|
api_password = util.convert(conf.get(CONF_API_PASSWORD), str)
|
||||||
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
|
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
|
||||||
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
|
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
|
||||||
development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
|
development = str(conf.get(CONF_DEVELOPMENT, '')) == '1'
|
||||||
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
||||||
ssl_key = conf.get(CONF_SSL_KEY)
|
ssl_key = conf.get(CONF_SSL_KEY)
|
||||||
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
||||||
@ -365,8 +368,8 @@ class HomeAssistantWSGI(object):
|
|||||||
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
|
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
|
||||||
self.server.ssl_adapter = ContextSSLAdapter(context)
|
self.server.ssl_adapter = ContextSSLAdapter(context)
|
||||||
|
|
||||||
threading.Thread(target=self.server.start, daemon=True,
|
threading.Thread(
|
||||||
name='WSGI-server').start()
|
target=self.server.start, daemon=True, name='WSGI-server').start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop the wsgi server."""
|
"""Stop the wsgi server."""
|
||||||
@ -391,10 +394,10 @@ class HomeAssistantWSGI(object):
|
|||||||
resp = ex.get_response(request.environ)
|
resp = ex.get_response(request.environ)
|
||||||
if request.accept_mimetypes.accept_json:
|
if request.accept_mimetypes.accept_json:
|
||||||
resp.data = json.dumps({
|
resp.data = json.dumps({
|
||||||
"result": "error",
|
'result': 'error',
|
||||||
"message": str(ex),
|
'message': str(ex),
|
||||||
})
|
})
|
||||||
resp.mimetype = "application/json"
|
resp.mimetype = CONTENT_TYPE_JSON
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def base_app(self, environ, start_response):
|
def base_app(self, environ, start_response):
|
||||||
@ -403,11 +406,11 @@ class HomeAssistantWSGI(object):
|
|||||||
response = self.dispatch_request(request)
|
response = self.dispatch_request(request)
|
||||||
|
|
||||||
if self.cors_origins:
|
if self.cors_origins:
|
||||||
cors_check = (environ.get("HTTP_ORIGIN") in self.cors_origins)
|
cors_check = (environ.get('HTTP_ORIGIN') in self.cors_origins)
|
||||||
cors_headers = ", ".join(ALLOWED_CORS_HEADERS)
|
cors_headers = ", ".join(ALLOWED_CORS_HEADERS)
|
||||||
if cors_check:
|
if cors_check:
|
||||||
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = \
|
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = \
|
||||||
environ.get("HTTP_ORIGIN")
|
environ.get('HTTP_ORIGIN')
|
||||||
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS] = \
|
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS] = \
|
||||||
cors_headers
|
cors_headers
|
||||||
|
|
||||||
@ -425,7 +428,7 @@ class HomeAssistantWSGI(object):
|
|||||||
# Strip out any cachebusting MD5 fingerprints
|
# Strip out any cachebusting MD5 fingerprints
|
||||||
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
|
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
|
||||||
if fingerprinted:
|
if fingerprinted:
|
||||||
environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups())
|
environ['PATH_INFO'] = '{}.{}'.format(*fingerprinted.groups())
|
||||||
return app(environ, start_response)
|
return app(environ, start_response)
|
||||||
|
|
||||||
|
|
||||||
@ -489,6 +492,10 @@ class HomeAssistantView(object):
|
|||||||
if self.requires_auth and not authenticated:
|
if self.requires_auth and not authenticated:
|
||||||
_LOGGER.warning('Login attempt or request with an invalid '
|
_LOGGER.warning('Login attempt or request with an invalid '
|
||||||
'password from %s', request.remote_addr)
|
'password from %s', request.remote_addr)
|
||||||
|
persistent_notification.create(
|
||||||
|
self.hass,
|
||||||
|
'Invalid password used from {}'.format(request.remote_addr),
|
||||||
|
'Login attempt failed', NOTIFICATION_ID_LOGIN)
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
request.authenticated = authenticated
|
request.authenticated = authenticated
|
||||||
@ -512,12 +519,9 @@ class HomeAssistantView(object):
|
|||||||
def json(self, result, status_code=200):
|
def json(self, result, status_code=200):
|
||||||
"""Return a JSON response."""
|
"""Return a JSON response."""
|
||||||
msg = json.dumps(
|
msg = json.dumps(
|
||||||
result,
|
result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8')
|
||||||
sort_keys=True,
|
return self.Response(
|
||||||
cls=rem.JSONEncoder
|
msg, mimetype=CONTENT_TYPE_JSON, status=status_code)
|
||||||
).encode('UTF-8')
|
|
||||||
return self.Response(msg, mimetype="application/json",
|
|
||||||
status=status_code)
|
|
||||||
|
|
||||||
def json_message(self, error, status_code=200):
|
def json_message(self, error, status_code=200):
|
||||||
"""Return a JSON message response."""
|
"""Return a JSON message response."""
|
||||||
|
@ -9,8 +9,7 @@ https://home-assistant.io/components/hvac.zwave/
|
|||||||
import logging
|
import logging
|
||||||
from homeassistant.components.hvac import DOMAIN
|
from homeassistant.components.hvac import DOMAIN
|
||||||
from homeassistant.components.hvac import HvacDevice
|
from homeassistant.components.hvac import HvacDevice
|
||||||
from homeassistant.components.zwave import (
|
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||||
ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity)
|
|
||||||
from homeassistant.components import zwave
|
from homeassistant.components import zwave
|
||||||
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||||
|
|
||||||
@ -23,12 +22,6 @@ REMOTEC = 0x5254
|
|||||||
REMOTEC_ZXT_120 = 0x8377
|
REMOTEC_ZXT_120 = 0x8377
|
||||||
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0)
|
REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0)
|
||||||
|
|
||||||
COMMAND_CLASS_SENSOR_MULTILEVEL = 0x31
|
|
||||||
COMMAND_CLASS_THERMOSTAT_MODE = 0x40
|
|
||||||
COMMAND_CLASS_THERMOSTAT_SETPOINT = 0x43
|
|
||||||
COMMAND_CLASS_THERMOSTAT_FAN_MODE = 0x44
|
|
||||||
COMMAND_CLASS_CONFIGURATION = 0x70
|
|
||||||
|
|
||||||
WORKAROUND_ZXT_120 = 'zxt_120'
|
WORKAROUND_ZXT_120 = 'zxt_120'
|
||||||
|
|
||||||
DEVICE_MAPPINGS = {
|
DEVICE_MAPPINGS = {
|
||||||
@ -50,8 +43,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
discovery_info, zwave.NETWORK)
|
discovery_info, zwave.NETWORK)
|
||||||
return
|
return
|
||||||
|
|
||||||
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
|
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||||
value = node.values[discovery_info[ATTR_VALUE_ID]]
|
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
add_devices([ZWaveHvac(value)])
|
add_devices([ZWaveHvac(value)])
|
||||||
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||||
@ -107,25 +100,29 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
|||||||
def update_properties(self):
|
def update_properties(self):
|
||||||
"""Callback on data change for the registered node/value pair."""
|
"""Callback on data change for the registered node/value pair."""
|
||||||
# Set point
|
# Set point
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||||
|
.values()):
|
||||||
if int(value.data) != 0:
|
if int(value.data) != 0:
|
||||||
self._target_temperature = int(value.data)
|
self._target_temperature = int(value.data)
|
||||||
# Operation Mode
|
# Operation Mode
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE)
|
||||||
|
.values()):
|
||||||
self._current_operation = value.data
|
self._current_operation = value.data
|
||||||
self._operation_list = list(value.data_items)
|
self._operation_list = list(value.data_items)
|
||||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||||
# Current Temp
|
# Current Temp
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_SENSOR_MULTILEVEL).values():
|
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL)
|
||||||
|
.values()):
|
||||||
if value.label == 'Temperature':
|
if value.label == 'Temperature':
|
||||||
self._current_temperature = int(value.data)
|
self._current_temperature = int(value.data)
|
||||||
self._unit = value.units
|
self._unit = value.units
|
||||||
# Fan Mode
|
# Fan Mode
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||||
|
.values()):
|
||||||
self._current_operation_state = value.data
|
self._current_operation_state = value.data
|
||||||
self._fan_list = list(value.data_items)
|
self._fan_list = list(value.data_items)
|
||||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||||
@ -133,8 +130,9 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
|||||||
self._current_operation_state)
|
self._current_operation_state)
|
||||||
# Swing mode
|
# Swing mode
|
||||||
if self._zxt_120 == 1:
|
if self._zxt_120 == 1:
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION)
|
||||||
|
.values()):
|
||||||
if value.command_class == 112 and value.index == 33:
|
if value.command_class == 112 and value.index == 33:
|
||||||
self._current_swing_mode = value.data
|
self._current_swing_mode = value.data
|
||||||
self._swing_list = list(value.data_items)
|
self._swing_list = list(value.data_items)
|
||||||
@ -199,8 +197,9 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
|||||||
|
|
||||||
def set_temperature(self, temperature):
|
def set_temperature(self, temperature):
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||||
|
.values()):
|
||||||
if value.command_class != 67:
|
if value.command_class != 67:
|
||||||
continue
|
continue
|
||||||
if self._zxt_120:
|
if self._zxt_120:
|
||||||
@ -217,8 +216,9 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
|||||||
|
|
||||||
def set_fan_mode(self, fan):
|
def set_fan_mode(self, fan):
|
||||||
"""Set new target fan mode."""
|
"""Set new target fan mode."""
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values():
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE)
|
||||||
|
.values()):
|
||||||
if value.command_class == 68 and value.index == 0:
|
if value.command_class == 68 and value.index == 0:
|
||||||
value.data = bytes(fan, 'utf-8')
|
value.data = bytes(fan, 'utf-8')
|
||||||
break
|
break
|
||||||
@ -226,7 +226,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
|||||||
def set_operation_mode(self, operation_mode):
|
def set_operation_mode(self, operation_mode):
|
||||||
"""Set new target operation mode."""
|
"""Set new target operation mode."""
|
||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
|
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values():
|
||||||
if value.command_class == 64 and value.index == 0:
|
if value.command_class == 64 and value.index == 0:
|
||||||
value.data = bytes(operation_mode, 'utf-8')
|
value.data = bytes(operation_mode, 'utf-8')
|
||||||
break
|
break
|
||||||
@ -235,7 +235,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
|||||||
"""Set new target swing mode."""
|
"""Set new target swing mode."""
|
||||||
if self._zxt_120 == 1:
|
if self._zxt_120 == 1:
|
||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION).values():
|
||||||
if value.command_class == 112 and value.index == 33:
|
if value.command_class == 112 and value.index == 33:
|
||||||
value.data = bytes(swing_mode, 'utf-8')
|
value.data = bytes(swing_mode, 'utf-8')
|
||||||
break
|
break
|
||||||
|
@ -28,6 +28,7 @@ DEFAULT_PORT = 8086
|
|||||||
DEFAULT_SSL = False
|
DEFAULT_SSL = False
|
||||||
DEFAULT_VERIFY_SSL = False
|
DEFAULT_VERIFY_SSL = False
|
||||||
DOMAIN = 'influxdb'
|
DOMAIN = 'influxdb'
|
||||||
|
TIMEOUT = 5
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
@ -69,7 +70,8 @@ def setup(hass, config):
|
|||||||
try:
|
try:
|
||||||
influx = InfluxDBClient(
|
influx = InfluxDBClient(
|
||||||
host=host, port=port, username=username, password=password,
|
host=host, port=port, username=username, password=password,
|
||||||
database=database, ssl=ssl, verify_ssl=verify_ssl)
|
database=database, ssl=ssl, verify_ssl=verify_ssl,
|
||||||
|
timeout=TIMEOUT)
|
||||||
influx.query("select * from /.*/ LIMIT 1;")
|
influx.query("select * from /.*/ LIMIT 1;")
|
||||||
except exceptions.InfluxDBClientError as exc:
|
except exceptions.InfluxDBClientError as exc:
|
||||||
_LOGGER.error("Database host is not accessible due to '%s', please "
|
_LOGGER.error("Database host is not accessible due to '%s', please "
|
||||||
|
@ -6,8 +6,14 @@ https://home-assistant.io/components/introduction/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
DOMAIN = 'introduction'
|
DOMAIN = 'introduction'
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({}),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config=None):
|
def setup(hass, config=None):
|
||||||
"""Setup the introduction component."""
|
"""Setup the introduction component."""
|
||||||
|
@ -93,7 +93,8 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({
|
|||||||
vol.Coerce(tuple)),
|
vol.Coerce(tuple)),
|
||||||
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
||||||
vol.Coerce(tuple)),
|
vol.Coerce(tuple)),
|
||||||
ATTR_COLOR_TEMP: vol.All(int, vol.Range(min=154, max=500)),
|
ATTR_COLOR_TEMP: vol.All(int, vol.Range(min=color_util.HASS_COLOR_MIN,
|
||||||
|
max=color_util.HASS_COLOR_MAX)),
|
||||||
ATTR_WHITE_VALUE: vol.All(int, vol.Range(min=0, max=255)),
|
ATTR_WHITE_VALUE: vol.All(int, vol.Range(min=0, max=255)),
|
||||||
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
|
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
|
||||||
ATTR_EFFECT: vol.In([EFFECT_COLORLOOP, EFFECT_RANDOM, EFFECT_WHITE]),
|
ATTR_EFFECT: vol.In([EFFECT_COLORLOOP, EFFECT_RANDOM, EFFECT_WHITE]),
|
||||||
|
@ -17,8 +17,8 @@ from homeassistant.components.light import (
|
|||||||
PLATFORM_SCHEMA)
|
PLATFORM_SCHEMA)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.6.zip'
|
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.7.zip'
|
||||||
'#flux_led==0.6']
|
'#flux_led==0.7']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -7,19 +7,35 @@ https://home-assistant.io/components/light.limitlessled/
|
|||||||
# pylint: disable=abstract-method
|
# pylint: disable=abstract-method
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT)
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
|
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
|
||||||
ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG,
|
ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG,
|
||||||
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH,
|
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH,
|
||||||
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light)
|
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
REQUIREMENTS = ['limitlessled==1.0.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = ['limitlessled==1.0.0']
|
|
||||||
RGB_BOUNDARY = 40
|
CONF_BRIDGES = 'bridges'
|
||||||
DEFAULT_TRANSITION = 0
|
CONF_GROUPS = 'groups'
|
||||||
DEFAULT_PORT = 8899
|
CONF_NUMBER = 'number'
|
||||||
DEFAULT_VERSION = 5
|
CONF_TYPE = 'type'
|
||||||
|
CONF_VERSION = 'version'
|
||||||
|
|
||||||
DEFAULT_LED_TYPE = 'rgbw'
|
DEFAULT_LED_TYPE = 'rgbw'
|
||||||
|
DEFAULT_PORT = 8899
|
||||||
|
DEFAULT_TRANSITION = 0
|
||||||
|
DEFAULT_VERSION = 5
|
||||||
|
|
||||||
|
LED_TYPE = ['rgbw', 'white']
|
||||||
|
|
||||||
|
RGB_BOUNDARY = 40
|
||||||
|
|
||||||
WHITE = [255, 255, 255]
|
WHITE = [255, 255, 255]
|
||||||
|
|
||||||
SUPPORT_LIMITLESSLED_WHITE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP |
|
SUPPORT_LIMITLESSLED_WHITE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP |
|
||||||
@ -28,6 +44,25 @@ SUPPORT_LIMITLESSLED_RGB = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
|
|||||||
SUPPORT_FLASH | SUPPORT_RGB_COLOR |
|
SUPPORT_FLASH | SUPPORT_RGB_COLOR |
|
||||||
SUPPORT_TRANSITION)
|
SUPPORT_TRANSITION)
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_BRIDGES): vol.All(cv.ensure_list, [
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Optional(CONF_VERSION,
|
||||||
|
default=DEFAULT_VERSION): cv.positive_int,
|
||||||
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
|
vol.Required(CONF_GROUPS): vol.All(cv.ensure_list, [
|
||||||
|
{
|
||||||
|
vol.Required(CONF_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_TYPE, default=DEFAULT_LED_TYPE):
|
||||||
|
vol.In(LED_TYPE),
|
||||||
|
vol.Required(CONF_NUMBER): cv.positive_int,
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def rewrite_legacy(config):
|
def rewrite_legacy(config):
|
||||||
"""Rewrite legacy configuration to new format."""
|
"""Rewrite legacy configuration to new format."""
|
||||||
|
@ -10,11 +10,12 @@ import voluptuous as vol
|
|||||||
|
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR,
|
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS,
|
||||||
Light)
|
SUPPORT_RGB_COLOR, SUPPORT_COLOR_TEMP, Light)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF,
|
CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF,
|
||||||
CONF_PAYLOAD_ON, CONF_STATE, CONF_BRIGHTNESS, CONF_RGB)
|
CONF_PAYLOAD_ON, CONF_STATE, CONF_BRIGHTNESS, CONF_RGB,
|
||||||
|
CONF_COLOR_TEMP)
|
||||||
from homeassistant.components.mqtt import (
|
from homeassistant.components.mqtt import (
|
||||||
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
|
CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -31,6 +32,9 @@ CONF_RGB_STATE_TOPIC = 'rgb_state_topic'
|
|||||||
CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic'
|
CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic'
|
||||||
CONF_RGB_VALUE_TEMPLATE = 'rgb_value_template'
|
CONF_RGB_VALUE_TEMPLATE = 'rgb_value_template'
|
||||||
CONF_BRIGHTNESS_SCALE = 'brightness_scale'
|
CONF_BRIGHTNESS_SCALE = 'brightness_scale'
|
||||||
|
CONF_COLOR_TEMP_STATE_TOPIC = 'color_temp_state_topic'
|
||||||
|
CONF_COLOR_TEMP_COMMAND_TOPIC = 'color_temp_command_topic'
|
||||||
|
CONF_COLOR_TEMP_VALUE_TEMPLATE = 'color_temp_value_template'
|
||||||
|
|
||||||
DEFAULT_NAME = 'MQTT Light'
|
DEFAULT_NAME = 'MQTT Light'
|
||||||
DEFAULT_PAYLOAD_ON = 'ON'
|
DEFAULT_PAYLOAD_ON = 'ON'
|
||||||
@ -44,6 +48,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
|||||||
vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||||
vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||||
vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template,
|
vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||||
|
vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||||
|
vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template,
|
||||||
vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||||
vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||||
vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template,
|
vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template,
|
||||||
@ -70,12 +77,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
CONF_BRIGHTNESS_COMMAND_TOPIC,
|
CONF_BRIGHTNESS_COMMAND_TOPIC,
|
||||||
CONF_RGB_STATE_TOPIC,
|
CONF_RGB_STATE_TOPIC,
|
||||||
CONF_RGB_COMMAND_TOPIC,
|
CONF_RGB_COMMAND_TOPIC,
|
||||||
|
CONF_COLOR_TEMP_STATE_TOPIC,
|
||||||
|
CONF_COLOR_TEMP_COMMAND_TOPIC
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
|
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
|
||||||
CONF_BRIGHTNESS: config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE),
|
CONF_BRIGHTNESS: config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE),
|
||||||
CONF_RGB: config.get(CONF_RGB_VALUE_TEMPLATE)
|
CONF_RGB: config.get(CONF_RGB_VALUE_TEMPLATE),
|
||||||
|
CONF_COLOR_TEMP: config.get(CONF_COLOR_TEMP_VALUE_TEMPLATE)
|
||||||
},
|
},
|
||||||
config.get(CONF_QOS),
|
config.get(CONF_QOS),
|
||||||
config.get(CONF_RETAIN),
|
config.get(CONF_RETAIN),
|
||||||
@ -92,6 +102,7 @@ class MqttLight(Light):
|
|||||||
"""MQTT light."""
|
"""MQTT light."""
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||||
|
# pylint: disable=too-many-locals,too-many-branches
|
||||||
def __init__(self, hass, name, topic, templates, qos, retain, payload,
|
def __init__(self, hass, name, topic, templates, qos, retain, payload,
|
||||||
optimistic, brightness_scale):
|
optimistic, brightness_scale):
|
||||||
"""Initialize MQTT light."""
|
"""Initialize MQTT light."""
|
||||||
@ -106,6 +117,8 @@ class MqttLight(Light):
|
|||||||
optimistic or topic[CONF_RGB_STATE_TOPIC] is None
|
optimistic or topic[CONF_RGB_STATE_TOPIC] is None
|
||||||
self._optimistic_brightness = (
|
self._optimistic_brightness = (
|
||||||
optimistic or topic[CONF_BRIGHTNESS_STATE_TOPIC] is None)
|
optimistic or topic[CONF_BRIGHTNESS_STATE_TOPIC] is None)
|
||||||
|
self._optimistic_color_temp = (
|
||||||
|
optimistic or topic[CONF_COLOR_TEMP_STATE_TOPIC] is None)
|
||||||
self._brightness_scale = brightness_scale
|
self._brightness_scale = brightness_scale
|
||||||
self._state = False
|
self._state = False
|
||||||
self._supported_features = 0
|
self._supported_features = 0
|
||||||
@ -114,6 +127,9 @@ class MqttLight(Light):
|
|||||||
self._supported_features |= (
|
self._supported_features |= (
|
||||||
topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None and
|
topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None and
|
||||||
SUPPORT_BRIGHTNESS)
|
SUPPORT_BRIGHTNESS)
|
||||||
|
self._supported_features |= (
|
||||||
|
topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None and
|
||||||
|
SUPPORT_COLOR_TEMP)
|
||||||
|
|
||||||
for key, tpl in list(templates.items()):
|
for key, tpl in list(templates.items()):
|
||||||
if tpl is None:
|
if tpl is None:
|
||||||
@ -168,6 +184,21 @@ class MqttLight(Light):
|
|||||||
else:
|
else:
|
||||||
self._rgb = None
|
self._rgb = None
|
||||||
|
|
||||||
|
def color_temp_received(topic, payload, qos):
|
||||||
|
"""A new MQTT message for color temp has been received."""
|
||||||
|
self._color_temp = int(templates[CONF_COLOR_TEMP](payload))
|
||||||
|
self.update_ha_state()
|
||||||
|
|
||||||
|
if self._topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None:
|
||||||
|
mqtt.subscribe(
|
||||||
|
self._hass, self._topic[CONF_COLOR_TEMP_STATE_TOPIC],
|
||||||
|
color_temp_received, self._qos)
|
||||||
|
self._color_temp = 150
|
||||||
|
if self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None:
|
||||||
|
self._color_temp = 150
|
||||||
|
else:
|
||||||
|
self._color_temp = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
"""Return the brightness of this light between 0..255."""
|
"""Return the brightness of this light between 0..255."""
|
||||||
@ -178,6 +209,11 @@ class MqttLight(Light):
|
|||||||
"""Return the RGB color value."""
|
"""Return the RGB color value."""
|
||||||
return self._rgb
|
return self._rgb
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_temp(self):
|
||||||
|
"""Return the color temperature in mired."""
|
||||||
|
return self._color_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""No polling needed for a MQTT light."""
|
"""No polling needed for a MQTT light."""
|
||||||
@ -230,6 +266,16 @@ class MqttLight(Light):
|
|||||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||||
should_update = True
|
should_update = True
|
||||||
|
|
||||||
|
if ATTR_COLOR_TEMP in kwargs and \
|
||||||
|
self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None:
|
||||||
|
color_temp = int(kwargs[ATTR_COLOR_TEMP])
|
||||||
|
mqtt.publish(
|
||||||
|
self._hass, self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC],
|
||||||
|
color_temp, self._qos, self._retain)
|
||||||
|
if self._optimistic_color_temp:
|
||||||
|
self._color_temp = kwargs[ATTR_COLOR_TEMP]
|
||||||
|
should_update = True
|
||||||
|
|
||||||
mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC],
|
mqtt.publish(self._hass, self._topic[CONF_COMMAND_TOPIC],
|
||||||
self._payload['on'], self._qos, self._retain)
|
self._payload['on'], self._qos, self._retain)
|
||||||
|
|
||||||
|
@ -50,19 +50,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None or zwave.NETWORK is None:
|
if discovery_info is None or zwave.NETWORK is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||||
|
|
||||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
||||||
return
|
return
|
||||||
if value.type != zwave.TYPE_BYTE:
|
if value.type != zwave.const.TYPE_BYTE:
|
||||||
return
|
return
|
||||||
if value.genre != zwave.GENRE_USER:
|
if value.genre != zwave.const.GENRE_USER:
|
||||||
return
|
return
|
||||||
|
|
||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
|
|
||||||
if node.has_command_class(zwave.COMMAND_CLASS_COLOR):
|
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
|
||||||
try:
|
try:
|
||||||
add_devices([ZwaveColorLight(value)])
|
add_devices([ZwaveColorLight(value)])
|
||||||
except ValueError as exception:
|
except ValueError as exception:
|
||||||
@ -195,8 +195,8 @@ class ZwaveColorLight(ZwaveDimmer):
|
|||||||
raise ValueError("No matching color command found.")
|
raise ValueError("No matching color command found.")
|
||||||
|
|
||||||
for value_color_channels in value.node.get_values(
|
for value_color_channels in value.node.get_values(
|
||||||
class_id=zwave.COMMAND_CLASS_COLOR, genre='System',
|
class_id=zwave.const.COMMAND_CLASS_SWITCH_COLOR,
|
||||||
type="Int").values():
|
genre='System', type="Int").values():
|
||||||
self._value_color_channels = value_color_channels
|
self._value_color_channels = value_color_channels
|
||||||
|
|
||||||
if self._value_color_channels is None:
|
if self._value_color_channels is None:
|
||||||
|
@ -1,28 +1,35 @@
|
|||||||
"""
|
"""
|
||||||
LIRC interface to receive signals from a infrared remote control.
|
LIRC interface to receive signals from a infrared remote control.
|
||||||
|
|
||||||
This sensor will momentarily set state to various values as defined
|
For more details about this component, please refer to the documentation at
|
||||||
in the .lintrc file which can be interpreted in home-assistant to
|
https://home-assistant.io/components/lirc/
|
||||||
trigger various actions.
|
|
||||||
|
|
||||||
Sending signals to other IR receivers can be accomplished with the
|
|
||||||
shell_command component and the irsend command for now.
|
|
||||||
"""
|
"""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
|
import voluptuous as vol
|
||||||
EVENT_HOMEASSISTANT_START)
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||||
|
|
||||||
DOMAIN = "lirc"
|
|
||||||
REQUIREMENTS = ['python-lirc==1.2.1']
|
REQUIREMENTS = ['python-lirc==1.2.1']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
ICON = 'mdi:remote'
|
|
||||||
EVENT_IR_COMMAND_RECEIVED = 'ir_command_received'
|
|
||||||
BUTTON_NAME = 'button_name'
|
BUTTON_NAME = 'button_name'
|
||||||
|
|
||||||
|
DOMAIN = 'lirc'
|
||||||
|
|
||||||
|
EVENT_IR_COMMAND_RECEIVED = 'ir_command_received'
|
||||||
|
|
||||||
|
ICON = 'mdi:remote'
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({}),
|
||||||
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup LIRC capability."""
|
"""Setup LIRC capability."""
|
||||||
@ -65,20 +72,19 @@ class LircInterface(threading.Thread):
|
|||||||
def run(self):
|
def run(self):
|
||||||
"""Main loop of LIRC interface thread."""
|
"""Main loop of LIRC interface thread."""
|
||||||
import lirc
|
import lirc
|
||||||
_LOGGER.debug('LIRC interface thread started')
|
_LOGGER.debug("LIRC interface thread started")
|
||||||
while not self.stopped.isSet():
|
while not self.stopped.isSet():
|
||||||
try:
|
try:
|
||||||
code = lirc.nextcode() # list; empty if no buttons pressed
|
code = lirc.nextcode() # list; empty if no buttons pressed
|
||||||
except lirc.NextCodeError:
|
except lirc.NextCodeError:
|
||||||
_LOGGER.warning('Encountered error reading '
|
_LOGGER.warning("Error reading next code from LIRC")
|
||||||
'next code from LIRC')
|
|
||||||
code = None
|
code = None
|
||||||
# interpret result from python-lirc
|
# interpret result from python-lirc
|
||||||
if code:
|
if code:
|
||||||
code = code[0]
|
code = code[0]
|
||||||
_LOGGER.info('Got new LIRC code %s', code)
|
_LOGGER.info("Got new LIRC code %s", code)
|
||||||
self.hass.bus.fire(EVENT_IR_COMMAND_RECEIVED,
|
self.hass.bus.fire(
|
||||||
{BUTTON_NAME: code})
|
EVENT_IR_COMMAND_RECEIVED, {BUTTON_NAME: code})
|
||||||
else:
|
else:
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
lirc.deinit()
|
lirc.deinit()
|
||||||
|
@ -16,14 +16,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None or zwave.NETWORK is None:
|
if discovery_info is None or zwave.NETWORK is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||||
|
|
||||||
if value.command_class != zwave.COMMAND_CLASS_DOOR_LOCK:
|
if value.command_class != zwave.const.COMMAND_CLASS_DOOR_LOCK:
|
||||||
return
|
return
|
||||||
if value.type != zwave.TYPE_BOOL:
|
if value.type != zwave.const.TYPE_BOOL:
|
||||||
return
|
return
|
||||||
if value.genre != zwave.GENRE_USER:
|
if value.genre != zwave.const.GENRE_USER:
|
||||||
return
|
return
|
||||||
|
|
||||||
value.set_change_verified(False)
|
value.set_change_verified(False)
|
||||||
|
@ -4,6 +4,7 @@ Event parser and human readable log generator.
|
|||||||
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/logbook/
|
https://home-assistant.io/components/logbook/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
@ -20,6 +21,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
|||||||
STATE_NOT_HOME, STATE_OFF, STATE_ON,
|
STATE_NOT_HOME, STATE_OFF, STATE_ON,
|
||||||
ATTR_HIDDEN)
|
ATTR_HIDDEN)
|
||||||
from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN
|
from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN
|
||||||
|
from homeassistant.util.async import run_callback_threadsafe
|
||||||
|
|
||||||
DOMAIN = "logbook"
|
DOMAIN = "logbook"
|
||||||
DEPENDENCIES = ['recorder', 'frontend']
|
DEPENDENCIES = ['recorder', 'frontend']
|
||||||
@ -57,6 +59,13 @@ LOG_MESSAGE_SCHEMA = vol.Schema({
|
|||||||
|
|
||||||
|
|
||||||
def log_entry(hass, name, message, domain=None, entity_id=None):
|
def log_entry(hass, name, message, domain=None, entity_id=None):
|
||||||
|
"""Add an entry to the logbook."""
|
||||||
|
run_callback_threadsafe(
|
||||||
|
hass.loop, async_log_entry, hass, name, message, domain, entity_id
|
||||||
|
).result()
|
||||||
|
|
||||||
|
|
||||||
|
def async_log_entry(hass, name, message, domain=None, entity_id=None):
|
||||||
"""Add an entry to the logbook."""
|
"""Add an entry to the logbook."""
|
||||||
data = {
|
data = {
|
||||||
ATTR_NAME: name,
|
ATTR_NAME: name,
|
||||||
@ -67,11 +76,12 @@ def log_entry(hass, name, message, domain=None, entity_id=None):
|
|||||||
data[ATTR_DOMAIN] = domain
|
data[ATTR_DOMAIN] = domain
|
||||||
if entity_id is not None:
|
if entity_id is not None:
|
||||||
data[ATTR_ENTITY_ID] = entity_id
|
data[ATTR_ENTITY_ID] = entity_id
|
||||||
hass.bus.fire(EVENT_LOGBOOK_ENTRY, data)
|
hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Listen for download events to download files."""
|
"""Listen for download events to download files."""
|
||||||
|
@asyncio.coroutine
|
||||||
def log_message(service):
|
def log_message(service):
|
||||||
"""Handle sending notification message service calls."""
|
"""Handle sending notification message service calls."""
|
||||||
message = service.data[ATTR_MESSAGE]
|
message = service.data[ATTR_MESSAGE]
|
||||||
@ -80,8 +90,8 @@ def setup(hass, config):
|
|||||||
entity_id = service.data.get(ATTR_ENTITY_ID)
|
entity_id = service.data.get(ATTR_ENTITY_ID)
|
||||||
|
|
||||||
message.hass = hass
|
message.hass = hass
|
||||||
message = message.render()
|
message = message.async_render()
|
||||||
log_entry(hass, name, message, domain, entity_id)
|
async_log_entry(hass, name, message, domain, entity_id)
|
||||||
|
|
||||||
hass.wsgi.register_view(LogbookView(hass, config))
|
hass.wsgi.register_view(LogbookView(hass, config))
|
||||||
|
|
||||||
|
@ -79,18 +79,11 @@ class LogitechMediaServer(object):
|
|||||||
|
|
||||||
def _get_http_port(self):
|
def _get_http_port(self):
|
||||||
"""Get http port from media server, it is used to get cover art."""
|
"""Get http port from media server, it is used to get cover art."""
|
||||||
http_port = None
|
http_port = self.query('pref', 'httpport', '?')
|
||||||
try:
|
if not http_port:
|
||||||
http_port = self.query('pref', 'httpport', '?')
|
_LOGGER.error("Failed to connect to server %s:%s",
|
||||||
if not http_port:
|
self.host, self.port)
|
||||||
_LOGGER.error("Unable to read data from server %s:%s",
|
return http_port
|
||||||
self.host, self.port)
|
|
||||||
return
|
|
||||||
return http_port
|
|
||||||
except ConnectionError as ex:
|
|
||||||
_LOGGER.error("Failed to connect to server %s:%s - %s",
|
|
||||||
self.host, self.port, ex)
|
|
||||||
return
|
|
||||||
|
|
||||||
def create_players(self):
|
def create_players(self):
|
||||||
"""Create a list of SqueezeBoxDevices connected to the LMS."""
|
"""Create a list of SqueezeBoxDevices connected to the LMS."""
|
||||||
@ -104,20 +97,27 @@ class LogitechMediaServer(object):
|
|||||||
|
|
||||||
def query(self, *parameters):
|
def query(self, *parameters):
|
||||||
"""Send request and await response from server."""
|
"""Send request and await response from server."""
|
||||||
telnet = telnetlib.Telnet(self.host, self.port)
|
try:
|
||||||
if self._username and self._password:
|
telnet = telnetlib.Telnet(self.host, self.port)
|
||||||
telnet.write('login {username} {password}\n'.format(
|
if self._username and self._password:
|
||||||
username=self._username,
|
telnet.write('login {username} {password}\n'.format(
|
||||||
password=self._password).encode('UTF-8'))
|
username=self._username,
|
||||||
telnet.read_until(b'\n', timeout=3)
|
password=self._password).encode('UTF-8'))
|
||||||
message = '{}\n'.format(' '.join(parameters))
|
telnet.read_until(b'\n', timeout=3)
|
||||||
telnet.write(message.encode('UTF-8'))
|
message = '{}\n'.format(' '.join(parameters))
|
||||||
response = telnet.read_until(b'\n', timeout=3)\
|
telnet.write(message.encode('UTF-8'))
|
||||||
.decode('UTF-8')\
|
response = telnet.read_until(b'\n', timeout=3)\
|
||||||
.split(' ')[-1]\
|
.decode('UTF-8')\
|
||||||
.strip()
|
.split(' ')[-1]\
|
||||||
telnet.write(b'exit\n')
|
.strip()
|
||||||
return urllib.parse.unquote(response)
|
telnet.write(b'exit\n')
|
||||||
|
return urllib.parse.unquote(response)
|
||||||
|
except (OSError, ConnectionError) as error:
|
||||||
|
_LOGGER.error("Could not communicate with %s:%d: %s",
|
||||||
|
self.host,
|
||||||
|
self.port,
|
||||||
|
error)
|
||||||
|
return None
|
||||||
|
|
||||||
def get_player_status(self, player):
|
def get_player_status(self, player):
|
||||||
"""Get ithe status of a player."""
|
"""Get ithe status of a player."""
|
||||||
@ -128,18 +128,24 @@ class LogitechMediaServer(object):
|
|||||||
# K (artwork_url): URL to remote artwork
|
# K (artwork_url): URL to remote artwork
|
||||||
tags = 'adK'
|
tags = 'adK'
|
||||||
new_status = {}
|
new_status = {}
|
||||||
telnet = telnetlib.Telnet(self.host, self.port)
|
try:
|
||||||
telnet.write('{player} status - 1 tags:{tags}\n'.format(
|
telnet = telnetlib.Telnet(self.host, self.port)
|
||||||
player=player,
|
telnet.write('{player} status - 1 tags:{tags}\n'.format(
|
||||||
tags=tags
|
player=player,
|
||||||
|
tags=tags
|
||||||
).encode('UTF-8'))
|
).encode('UTF-8'))
|
||||||
response = telnet.read_until(b'\n', timeout=3)\
|
response = telnet.read_until(b'\n', timeout=3)\
|
||||||
.decode('UTF-8')\
|
.decode('UTF-8')\
|
||||||
.split(' ')
|
.split(' ')
|
||||||
telnet.write(b'exit\n')
|
telnet.write(b'exit\n')
|
||||||
for item in response:
|
for item in response:
|
||||||
parts = urllib.parse.unquote(item).partition(':')
|
parts = urllib.parse.unquote(item).partition(':')
|
||||||
new_status[parts[0]] = parts[2]
|
new_status[parts[0]] = parts[2]
|
||||||
|
except (OSError, ConnectionError) as error:
|
||||||
|
_LOGGER.error("Could not communicate with %s:%d: %s",
|
||||||
|
self.host,
|
||||||
|
self.port,
|
||||||
|
error)
|
||||||
return new_status
|
return new_status
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,24 +8,31 @@ import logging
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
||||||
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||||
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
|
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
|
||||||
MediaPlayerDevice)
|
MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_CUSTOMIZE, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
|
CONF_HOST, CONF_CUSTOMIZE, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
|
||||||
STATE_UNKNOWN)
|
STATE_UNKNOWN, CONF_NAME)
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
_CONFIGURING = {}
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv'
|
REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv'
|
||||||
'/archive/v0.1.2.zip'
|
'/archive/v0.1.2.zip'
|
||||||
'#pylgtv==0.1.2']
|
'#pylgtv==0.1.2']
|
||||||
|
|
||||||
|
_CONFIGURING = {}
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_SOURCES = 'sources'
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'LG WebOS Smart TV'
|
||||||
|
|
||||||
SUPPORT_WEBOSTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
SUPPORT_WEBOSTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
||||||
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
||||||
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \
|
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \
|
||||||
@ -44,6 +51,17 @@ WEBOS_APPS_SHORT = {
|
|||||||
'makotv': WEBOS_APP_MAKO
|
'makotv': WEBOS_APP_MAKO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CONF_SOURCES):
|
||||||
|
vol.All(cv.ensure_list, [vol.In(WEBOS_APPS_SHORT)]),
|
||||||
|
})
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_HOST): cv.string,
|
||||||
|
vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# 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):
|
||||||
@ -51,21 +69,22 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is not None:
|
if discovery_info is not None:
|
||||||
host = urlparse(discovery_info[1]).hostname
|
host = urlparse(discovery_info[1]).hostname
|
||||||
else:
|
else:
|
||||||
host = config.get(CONF_HOST, None)
|
host = config.get(CONF_HOST)
|
||||||
|
|
||||||
if host is None:
|
if host is None:
|
||||||
_LOGGER.error('No host found in configuration')
|
_LOGGER.error("No host found in configuration")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Only act if we are not already configuring this host
|
# Only act if we are not already configuring this host
|
||||||
if host in _CONFIGURING:
|
if host in _CONFIGURING:
|
||||||
return
|
return
|
||||||
|
|
||||||
customize = config.get(CONF_CUSTOMIZE, {})
|
name = config.get(CONF_NAME)
|
||||||
setup_tv(host, customize, hass, add_devices)
|
customize = config.get(CONF_CUSTOMIZE)
|
||||||
|
setup_tv(host, name, customize, hass, add_devices)
|
||||||
|
|
||||||
|
|
||||||
def setup_tv(host, customize, hass, add_devices):
|
def setup_tv(host, name, customize, hass, add_devices):
|
||||||
"""Setup a phue bridge based on host parameter."""
|
"""Setup a phue bridge based on host parameter."""
|
||||||
from pylgtv import WebOsClient
|
from pylgtv import WebOsClient
|
||||||
from pylgtv import PyLGTVPairException
|
from pylgtv import PyLGTVPairException
|
||||||
@ -79,15 +98,15 @@ def setup_tv(host, customize, hass, add_devices):
|
|||||||
client.register()
|
client.register()
|
||||||
except PyLGTVPairException:
|
except PyLGTVPairException:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
'Connected to LG WebOS TV at %s but not paired.', host)
|
"Connected to LG WebOS TV at %s but not paired", host)
|
||||||
return
|
return
|
||||||
except OSError:
|
except OSError:
|
||||||
_LOGGER.error('Unable to connect to host %s.', host)
|
_LOGGER.error("Unable to connect to host %s", host)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
# Not registered, request configuration.
|
# Not registered, request configuration.
|
||||||
_LOGGER.warning('LG WebOS TV at %s needs to be paired.', host)
|
_LOGGER.warning('LG WebOS TV at %s needs to be paired.', host)
|
||||||
request_configuration(host, customize, hass, add_devices)
|
request_configuration(host, name, customize, hass, add_devices)
|
||||||
return
|
return
|
||||||
|
|
||||||
# If we came here and configuring this host, mark as done.
|
# If we came here and configuring this host, mark as done.
|
||||||
@ -96,10 +115,10 @@ def setup_tv(host, customize, hass, add_devices):
|
|||||||
configurator = get_component('configurator')
|
configurator = get_component('configurator')
|
||||||
configurator.request_done(request_id)
|
configurator.request_done(request_id)
|
||||||
|
|
||||||
add_devices([LgWebOSDevice(host, customize)])
|
add_devices([LgWebOSDevice(host, name, customize)])
|
||||||
|
|
||||||
|
|
||||||
def request_configuration(host, customize, hass, add_devices):
|
def request_configuration(host, name, customize, hass, add_devices):
|
||||||
"""Request configuration steps from the user."""
|
"""Request configuration steps from the user."""
|
||||||
configurator = get_component('configurator')
|
configurator = get_component('configurator')
|
||||||
|
|
||||||
@ -112,7 +131,7 @@ def request_configuration(host, customize, hass, add_devices):
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def lgtv_configuration_callback(data):
|
def lgtv_configuration_callback(data):
|
||||||
"""The actions to do when our configuration callback is called."""
|
"""The actions to do when our configuration callback is called."""
|
||||||
setup_tv(host, customize, hass, add_devices)
|
setup_tv(host, name, customize, hass, add_devices)
|
||||||
|
|
||||||
_CONFIGURING[host] = configurator.request_config(
|
_CONFIGURING[host] = configurator.request_config(
|
||||||
hass, 'LG WebOS TV', lgtv_configuration_callback,
|
hass, 'LG WebOS TV', lgtv_configuration_callback,
|
||||||
@ -128,13 +147,13 @@ class LgWebOSDevice(MediaPlayerDevice):
|
|||||||
"""Representation of a LG WebOS TV."""
|
"""Representation of a LG WebOS TV."""
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods
|
# pylint: disable=too-many-public-methods
|
||||||
def __init__(self, host, customize):
|
def __init__(self, host, name, customize):
|
||||||
"""Initialize the webos device."""
|
"""Initialize the webos device."""
|
||||||
from pylgtv import WebOsClient
|
from pylgtv import WebOsClient
|
||||||
self._client = WebOsClient(host)
|
self._client = WebOsClient(host)
|
||||||
self._customize = customize
|
self._customize = customize
|
||||||
|
|
||||||
self._name = 'LG WebOS TV Remote'
|
self._name = name
|
||||||
# Assume that the TV is not muted
|
# Assume that the TV is not muted
|
||||||
self._muted = False
|
self._muted = False
|
||||||
# Assume that the TV is in Play mode
|
# Assume that the TV is in Play mode
|
||||||
@ -160,7 +179,7 @@ class LgWebOSDevice(MediaPlayerDevice):
|
|||||||
self._app_list = {}
|
self._app_list = {}
|
||||||
|
|
||||||
custom_sources = []
|
custom_sources = []
|
||||||
for source in self._customize.get('sources', []):
|
for source in self._customize.get(CONF_SOURCES, []):
|
||||||
app_id = WEBOS_APPS_SHORT.get(source, None)
|
app_id = WEBOS_APPS_SHORT.get(source, None)
|
||||||
if app_id:
|
if app_id:
|
||||||
custom_sources.append(app_id)
|
custom_sources.append(app_id)
|
||||||
|
@ -4,6 +4,7 @@ Support for MQTT message handling.
|
|||||||
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/mqtt/
|
https://home-assistant.io/components/mqtt/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
@ -14,8 +15,8 @@ import voluptuous as vol
|
|||||||
from homeassistant.bootstrap import prepare_setup_platform
|
from homeassistant.bootstrap import prepare_setup_platform
|
||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template, config_validation as cv
|
||||||
import homeassistant.helpers.config_validation as cv
|
from homeassistant.helpers.event import threaded_listener_factory
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||||
CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_VALUE_TEMPLATE)
|
CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_VALUE_TEMPLATE)
|
||||||
@ -162,21 +163,28 @@ def publish_template(hass, topic, payload_template, qos=None, retain=None):
|
|||||||
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
|
hass.services.call(DOMAIN, SERVICE_PUBLISH, data)
|
||||||
|
|
||||||
|
|
||||||
def subscribe(hass, topic, callback, qos=DEFAULT_QOS):
|
def async_subscribe(hass, topic, callback, qos=DEFAULT_QOS):
|
||||||
"""Subscribe to an MQTT topic."""
|
"""Subscribe to an MQTT topic."""
|
||||||
|
@asyncio.coroutine
|
||||||
def mqtt_topic_subscriber(event):
|
def mqtt_topic_subscriber(event):
|
||||||
"""Match subscribed MQTT topic."""
|
"""Match subscribed MQTT topic."""
|
||||||
if _match_topic(topic, event.data[ATTR_TOPIC]):
|
if not _match_topic(topic, event.data[ATTR_TOPIC]):
|
||||||
callback(event.data[ATTR_TOPIC], event.data[ATTR_PAYLOAD],
|
return
|
||||||
event.data[ATTR_QOS])
|
|
||||||
|
|
||||||
remove = hass.bus.listen(EVENT_MQTT_MESSAGE_RECEIVED,
|
hass.async_run_job(callback, event.data[ATTR_TOPIC],
|
||||||
mqtt_topic_subscriber)
|
event.data[ATTR_PAYLOAD], event.data[ATTR_QOS])
|
||||||
|
|
||||||
|
async_remove = hass.bus.async_listen(EVENT_MQTT_MESSAGE_RECEIVED,
|
||||||
|
mqtt_topic_subscriber)
|
||||||
|
|
||||||
# Future: track subscriber count and unsubscribe in remove
|
# Future: track subscriber count and unsubscribe in remove
|
||||||
MQTT_CLIENT.subscribe(topic, qos)
|
MQTT_CLIENT.subscribe(topic, qos)
|
||||||
|
|
||||||
return remove
|
return async_remove
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
subscribe = threaded_listener_factory(async_subscribe)
|
||||||
|
|
||||||
|
|
||||||
def _setup_server(hass, config):
|
def _setup_server(hass, config):
|
||||||
|
@ -158,7 +158,8 @@ def setup(hass, config): # pylint: disable=too-many-locals
|
|||||||
'No devices could be setup as gateways, check your configuration')
|
'No devices could be setup as gateways, check your configuration')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for component in 'sensor', 'switch', 'light', 'binary_sensor', 'climate':
|
for component in ['sensor', 'switch', 'light', 'binary_sensor', 'climate',
|
||||||
|
'cover']:
|
||||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -340,5 +341,7 @@ class MySensorsDeviceEntity(object):
|
|||||||
set_req.V_LOCK_STATUS, set_req.V_TRIPPED):
|
set_req.V_LOCK_STATUS, set_req.V_TRIPPED):
|
||||||
self._values[value_type] = (
|
self._values[value_type] = (
|
||||||
STATE_ON if int(value) == 1 else STATE_OFF)
|
STATE_ON if int(value) == 1 else STATE_OFF)
|
||||||
|
elif value_type == set_req.V_DIMMER:
|
||||||
|
self._values[value_type] = int(value)
|
||||||
else:
|
else:
|
||||||
self._values[value_type] = value
|
self._values[value_type] = value
|
||||||
|
@ -14,7 +14,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, CONF_STRUCTURE)
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['python-nest==2.9.2']
|
REQUIREMENTS = ['python-nest==2.10.0']
|
||||||
|
|
||||||
DOMAIN = 'nest'
|
DOMAIN = 'nest'
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup the Netatmo devices."""
|
"""Set up the Netatmo devices."""
|
||||||
import lnetatmo
|
import lnetatmo
|
||||||
|
|
||||||
global NETATMO_AUTH
|
global NETATMO_AUTH
|
||||||
@ -47,7 +47,7 @@ def setup(hass, config):
|
|||||||
config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD],
|
config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD],
|
||||||
'read_station read_camera access_camera')
|
'read_station read_camera access_camera')
|
||||||
except HTTPError:
|
except HTTPError:
|
||||||
_LOGGER.error("Unable to connect to NatAtmo API")
|
_LOGGER.error("Unable to connect to Netatmo API")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for component in 'camera', 'sensor':
|
for component in 'camera', 'sensor':
|
||||||
|
@ -42,7 +42,7 @@ PLATFORM_SCHEMA = vol.Schema({
|
|||||||
NOTIFY_SERVICE_SCHEMA = vol.Schema({
|
NOTIFY_SERVICE_SCHEMA = vol.Schema({
|
||||||
vol.Required(ATTR_MESSAGE): cv.template,
|
vol.Required(ATTR_MESSAGE): cv.template,
|
||||||
vol.Optional(ATTR_TITLE): cv.template,
|
vol.Optional(ATTR_TITLE): cv.template,
|
||||||
vol.Optional(ATTR_TARGET): cv.string,
|
vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Optional(ATTR_DATA): dict,
|
vol.Optional(ATTR_DATA): dict,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -79,9 +79,6 @@ class AWSLambda(BaseNotificationService):
|
|||||||
_LOGGER.info("At least 1 target is required")
|
_LOGGER.info("At least 1 target is required")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(targets, list):
|
|
||||||
targets = [targets]
|
|
||||||
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v)
|
cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v)
|
||||||
payload = {"message": message}
|
payload = {"message": message}
|
||||||
|
@ -70,9 +70,6 @@ class AWSSNS(BaseNotificationService):
|
|||||||
_LOGGER.info("At least 1 target is required")
|
_LOGGER.info("At least 1 target is required")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(targets, list):
|
|
||||||
targets = [targets]
|
|
||||||
|
|
||||||
message_attributes = {k: {"StringValue": json.dumps(v),
|
message_attributes = {k: {"StringValue": json.dumps(v),
|
||||||
"DataType": "String"}
|
"DataType": "String"}
|
||||||
for k, v in kwargs.items() if v}
|
for k, v in kwargs.items() if v}
|
||||||
|
@ -69,9 +69,6 @@ class AWSSQS(BaseNotificationService):
|
|||||||
_LOGGER.info("At least 1 target is required")
|
_LOGGER.info("At least 1 target is required")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(targets, list):
|
|
||||||
targets = [targets]
|
|
||||||
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v)
|
cleaned_kwargs = dict((k, v) for k, v in kwargs.items() if v)
|
||||||
message_body = {"message": message}
|
message_body = {"message": message}
|
||||||
|
@ -359,8 +359,6 @@ class HTML5NotificationService(BaseNotificationService):
|
|||||||
|
|
||||||
if not targets:
|
if not targets:
|
||||||
targets = self.registrations.keys()
|
targets = self.registrations.keys()
|
||||||
elif not isinstance(targets, list):
|
|
||||||
targets = [targets]
|
|
||||||
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
info = self.registrations.get(target)
|
info = self.registrations.get(target)
|
||||||
|
@ -58,9 +58,6 @@ class MessageBirdNotificationService(BaseNotificationService):
|
|||||||
_LOGGER.error('No target specified.')
|
_LOGGER.error('No target specified.')
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(targets, list):
|
|
||||||
targets = [targets]
|
|
||||||
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
try:
|
try:
|
||||||
self.client.message_create(self.sender,
|
self.client.message_create(self.sender,
|
||||||
|
@ -87,10 +87,6 @@ class PushBulletNotificationService(BaseNotificationService):
|
|||||||
_LOGGER.info('Sent notification to self')
|
_LOGGER.info('Sent notification to self')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Make list if not so
|
|
||||||
if not isinstance(targets, list):
|
|
||||||
targets = [targets]
|
|
||||||
|
|
||||||
# Main loop, Process all targets specified
|
# Main loop, Process all targets specified
|
||||||
for target in targets:
|
for target in targets:
|
||||||
try:
|
try:
|
||||||
|
@ -18,45 +18,51 @@ REQUIREMENTS = ['pushetta==1.0.15']
|
|||||||
|
|
||||||
|
|
||||||
CONF_CHANNEL_NAME = 'channel_name'
|
CONF_CHANNEL_NAME = 'channel_name'
|
||||||
|
CONF_SEND_TEST_MSG = 'send_test_msg'
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_API_KEY): cv.string,
|
vol.Required(CONF_API_KEY): cv.string,
|
||||||
vol.Required(CONF_CHANNEL_NAME): cv.string,
|
vol.Required(CONF_CHANNEL_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_SEND_TEST_MSG, default=False): cv.boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def get_service(hass, config):
|
def get_service(hass, config):
|
||||||
"""Get the Pushetta notification service."""
|
"""Get the Pushetta notification service."""
|
||||||
from pushetta import Pushetta, exceptions
|
pushetta_service = PushettaNotificationService(config[CONF_API_KEY],
|
||||||
|
config[CONF_CHANNEL_NAME],
|
||||||
|
config[CONF_SEND_TEST_MSG])
|
||||||
|
|
||||||
try:
|
if pushetta_service.is_valid:
|
||||||
pushetta = Pushetta(config[CONF_API_KEY])
|
return pushetta_service
|
||||||
pushetta.pushMessage(config[CONF_CHANNEL_NAME],
|
|
||||||
"Home Assistant started")
|
|
||||||
except exceptions.TokenValidationError:
|
|
||||||
_LOGGER.error("Please check your access token")
|
|
||||||
return None
|
|
||||||
except exceptions.ChannelNotFoundError:
|
|
||||||
_LOGGER.error("Channel '%s' not found", config[CONF_CHANNEL_NAME])
|
|
||||||
return None
|
|
||||||
|
|
||||||
return PushettaNotificationService(config[CONF_API_KEY],
|
|
||||||
config[CONF_CHANNEL_NAME])
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
class PushettaNotificationService(BaseNotificationService):
|
class PushettaNotificationService(BaseNotificationService):
|
||||||
"""Implement the notification service for Pushetta."""
|
"""Implement the notification service for Pushetta."""
|
||||||
|
|
||||||
def __init__(self, api_key, channel_name):
|
def __init__(self, api_key, channel_name, send_test_msg):
|
||||||
"""Initialize the service."""
|
"""Initialize the service."""
|
||||||
from pushetta import Pushetta
|
from pushetta import Pushetta
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._channel_name = channel_name
|
self._channel_name = channel_name
|
||||||
self.pushetta = Pushetta(self._api_key)
|
self.is_valid = True
|
||||||
|
self.pushetta = Pushetta(api_key)
|
||||||
|
|
||||||
|
if send_test_msg:
|
||||||
|
self.send_message("Home Assistant started")
|
||||||
|
|
||||||
def send_message(self, message="", **kwargs):
|
def send_message(self, message="", **kwargs):
|
||||||
"""Send a message to a user."""
|
"""Send a message to a user."""
|
||||||
|
from pushetta import exceptions
|
||||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||||
self.pushetta.pushMessage(self._channel_name,
|
|
||||||
"{} {}".format(title, message))
|
try:
|
||||||
|
self.pushetta.pushMessage(self._channel_name,
|
||||||
|
"{} {}".format(title, message))
|
||||||
|
except exceptions.TokenValidationError:
|
||||||
|
_LOGGER.error("Please check your access token")
|
||||||
|
self.is_valid = False
|
||||||
|
except exceptions.ChannelNotFoundError:
|
||||||
|
_LOGGER.error("Channel '%s' not found", self._channel_name)
|
||||||
|
self.is_valid = False
|
||||||
|
@ -61,13 +61,15 @@ class PushoverNotificationService(BaseNotificationService):
|
|||||||
|
|
||||||
data['title'] = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
data['title'] = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||||
|
|
||||||
target = kwargs.get(ATTR_TARGET)
|
targets = kwargs.get(ATTR_TARGET)
|
||||||
if target is not None:
|
|
||||||
data['device'] = target
|
|
||||||
|
|
||||||
try:
|
for target in targets:
|
||||||
self.pushover.send_message(message, **data)
|
if target is not None:
|
||||||
except ValueError as val_err:
|
data['device'] = target
|
||||||
_LOGGER.error(str(val_err))
|
|
||||||
except RequestError:
|
try:
|
||||||
_LOGGER.exception('Could not send pushover notification')
|
self.pushover.send_message(message, **data)
|
||||||
|
except ValueError as val_err:
|
||||||
|
_LOGGER.error(str(val_err))
|
||||||
|
except RequestError:
|
||||||
|
_LOGGER.exception('Could not send pushover notification')
|
||||||
|
@ -75,8 +75,10 @@ class RestNotificationService(BaseNotificationService):
|
|||||||
data[self._title_param_name] = kwargs.get(ATTR_TITLE,
|
data[self._title_param_name] = kwargs.get(ATTR_TITLE,
|
||||||
ATTR_TITLE_DEFAULT)
|
ATTR_TITLE_DEFAULT)
|
||||||
|
|
||||||
if self._target_param_name is not None:
|
if self._target_param_name is not None and ATTR_TARGET in kwargs:
|
||||||
data[self._target_param_name] = kwargs.get(ATTR_TARGET)
|
# Target is a list as of 0.29 and we don't want to break existing
|
||||||
|
# integrations, so just return the first target in the list.
|
||||||
|
data[self._target_param_name] = kwargs[ATTR_TARGET][0]
|
||||||
|
|
||||||
if self._method == 'POST':
|
if self._method == 'POST':
|
||||||
response = requests.post(self._resource, data=data, timeout=10)
|
response = requests.post(self._resource, data=data, timeout=10)
|
||||||
|
@ -11,9 +11,9 @@ notify:
|
|||||||
example: 'Your Garage Door Friend'
|
example: 'Your Garage Door Friend'
|
||||||
|
|
||||||
target:
|
target:
|
||||||
description: Target of the notification. Optional depending on the platform
|
description: An array of targets to send the notification to. Optional depending on the platform.
|
||||||
example: platform specific
|
example: platform specific
|
||||||
|
|
||||||
data:
|
data:
|
||||||
description: Extended information for notification. Optional depending on the platform
|
description: Extended information for notification. Optional depending on the platform.
|
||||||
example: platform specific
|
example: platform specific
|
||||||
|
@ -9,7 +9,7 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.notify import (
|
from homeassistant.components.notify import (
|
||||||
PLATFORM_SCHEMA, BaseNotificationService)
|
ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
|
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -68,16 +68,19 @@ class SlackNotificationService(BaseNotificationService):
|
|||||||
"""Send a message to a user."""
|
"""Send a message to a user."""
|
||||||
import slacker
|
import slacker
|
||||||
|
|
||||||
channel = kwargs.get('target') or self._default_channel
|
targets = kwargs.get(ATTR_TARGET, [self._default_channel])
|
||||||
|
|
||||||
data = kwargs.get('data')
|
data = kwargs.get('data')
|
||||||
attachments = data.get('attachments') if data else None
|
attachments = data.get('attachments') if data else None
|
||||||
|
|
||||||
try:
|
for target in targets:
|
||||||
self.slack.chat.post_message(channel, message,
|
try:
|
||||||
as_user=self._as_user,
|
self.slack.chat.post_message(target, message,
|
||||||
username=self._username,
|
as_user=self._as_user,
|
||||||
icon_emoji=self._icon,
|
username=self._username,
|
||||||
attachments=attachments,
|
icon_emoji=self._icon,
|
||||||
link_names=True)
|
attachments=attachments,
|
||||||
except slacker.Error as err:
|
link_names=True)
|
||||||
_LOGGER.error("Could not send slack notification. Error: %s", err)
|
except slacker.Error as err:
|
||||||
|
_LOGGER.error("Could not send slack notification. Error: %s",
|
||||||
|
err)
|
||||||
|
@ -57,9 +57,6 @@ class TwilioSMSNotificationService(BaseNotificationService):
|
|||||||
_LOGGER.info("At least 1 target is required")
|
_LOGGER.info("At least 1 target is required")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(targets, list):
|
|
||||||
targets = [targets]
|
|
||||||
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
self.client.messages.create(to=target, body=message,
|
self.client.messages.create(to=target, body=message,
|
||||||
from_=self.from_number)
|
from_=self.from_number)
|
||||||
|
@ -27,7 +27,7 @@ DEPENDENCIES = ['ffmpeg']
|
|||||||
REQUIREMENTS = [
|
REQUIREMENTS = [
|
||||||
'https://github.com/pvizeli/cloudapi/releases/download/1.0.2/'
|
'https://github.com/pvizeli/cloudapi/releases/download/1.0.2/'
|
||||||
'python-1.0.2.zip#cloud_api==1.0.2',
|
'python-1.0.2.zip#cloud_api==1.0.2',
|
||||||
'ha-alpr==0.2']
|
'ha-alpr==0.3']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ DEFAULT_NAME = 'OpenAlpr'
|
|||||||
DEFAULT_ENGINE = ENGINE_LOCAL
|
DEFAULT_ENGINE = ENGINE_LOCAL
|
||||||
DEFAULT_RENDER = RENDER_FFMPEG
|
DEFAULT_RENDER = RENDER_FFMPEG
|
||||||
DEFAULT_BINARY = 'alpr'
|
DEFAULT_BINARY = 'alpr'
|
||||||
DEFAULT_INTERVAL = 2
|
DEFAULT_INTERVAL = 10
|
||||||
DEFAULT_CONFIDENCE = 80.0
|
DEFAULT_CONFIDENCE = 80.0
|
||||||
|
|
||||||
DEVICE_SCHEMA = vol.Schema({
|
DEVICE_SCHEMA = vol.Schema({
|
||||||
|
@ -12,9 +12,6 @@ from homeassistant.components.zwave import ZWaveDeviceEntity
|
|||||||
from homeassistant.components import zwave
|
from homeassistant.components import zwave
|
||||||
from homeassistant.components.rollershutter import RollershutterDevice
|
from homeassistant.components.rollershutter import RollershutterDevice
|
||||||
|
|
||||||
COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38
|
|
||||||
COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37
|
|
||||||
|
|
||||||
SOMFY = 0x47
|
SOMFY = 0x47
|
||||||
SOMFY_ZRTSI = 0x5a52
|
SOMFY_ZRTSI = 0x5a52
|
||||||
SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI)
|
SOMFY_ZRTSI_CONTROLLER = (SOMFY, SOMFY_ZRTSI)
|
||||||
@ -32,10 +29,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if discovery_info is None or zwave.NETWORK is None:
|
if discovery_info is None or zwave.NETWORK is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]]
|
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
|
||||||
value = node.values[discovery_info[zwave.ATTR_VALUE_ID]]
|
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
|
||||||
|
|
||||||
if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL:
|
||||||
return
|
return
|
||||||
if value.index != 0:
|
if value.index != 0:
|
||||||
return
|
return
|
||||||
@ -82,9 +79,10 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
|||||||
"""Callback on data change for the registered node/value pair."""
|
"""Callback on data change for the registered node/value pair."""
|
||||||
# Position value
|
# Position value
|
||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
if value.command_class == \
|
||||||
and value.label == 'Level':
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and \
|
||||||
|
value.label == 'Level':
|
||||||
self._current_position = value.data
|
self._current_position = value.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -101,23 +99,26 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
|||||||
|
|
||||||
def move_up(self, **kwargs):
|
def move_up(self, **kwargs):
|
||||||
"""Move the roller shutter up."""
|
"""Move the roller shutter up."""
|
||||||
for value in self._node.get_values(
|
for value in (self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL)
|
||||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
.values()):
|
||||||
and value.label == 'Open' or \
|
if value.command_class == \
|
||||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
and value.label == 'Down':
|
'Open' or value.command_class == \
|
||||||
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
|
'Down':
|
||||||
self._lozwmgr.pressButton(value.value_id)
|
self._lozwmgr.pressButton(value.value_id)
|
||||||
break
|
break
|
||||||
|
|
||||||
def move_down(self, **kwargs):
|
def move_down(self, **kwargs):
|
||||||
"""Move the roller shutter down."""
|
"""Move the roller shutter down."""
|
||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
if value.command_class == \
|
||||||
and value.label == 'Up' or \
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
'Up' or value.command_class == \
|
||||||
and value.label == 'Close':
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
|
'Close':
|
||||||
self._lozwmgr.pressButton(value.value_id)
|
self._lozwmgr.pressButton(value.value_id)
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -128,10 +129,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice):
|
|||||||
def stop(self, **kwargs):
|
def stop(self, **kwargs):
|
||||||
"""Stop the roller shutter."""
|
"""Stop the roller shutter."""
|
||||||
for value in self._node.get_values(
|
for value in self._node.get_values(
|
||||||
class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
|
||||||
if value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
if value.command_class == \
|
||||||
and value.label == 'Open' or \
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
value.command_class == zwave.COMMAND_CLASS_SWITCH_MULTILEVEL \
|
'Open' or value.command_class == \
|
||||||
and value.label == 'Down':
|
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
|
||||||
|
'Down':
|
||||||
self._lozwmgr.releaseButton(value.value_id)
|
self._lozwmgr.releaseButton(value.value_id)
|
||||||
break
|
break
|
||||||
|
@ -12,7 +12,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = [
|
REQUIREMENTS = [
|
||||||
'https://github.com/sander76/powerviewApi/'
|
'https://github.com/sander76/powerviewApi/'
|
||||||
'archive/master.zip#powerviewApi==0.2']
|
'archive/cc6f75dd39160d4aaf46cb2ed9220136b924bcb4.zip#powerviewApi==0.2']
|
||||||
|
|
||||||
HUB_ADDRESS = 'address'
|
HUB_ADDRESS = 'address'
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ from homeassistant.helpers.script import Script
|
|||||||
|
|
||||||
DOMAIN = "script"
|
DOMAIN = "script"
|
||||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
GROUP_NAME_ALL_SCRIPTS = 'all scripts'
|
||||||
DEPENDENCIES = ["group"]
|
DEPENDENCIES = ["group"]
|
||||||
|
|
||||||
CONF_SEQUENCE = "sequence"
|
CONF_SEQUENCE = "sequence"
|
||||||
@ -73,7 +74,8 @@ def toggle(hass, entity_id):
|
|||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Load the scripts from the configuration."""
|
"""Load the scripts from the configuration."""
|
||||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||||
|
group_name=GROUP_NAME_ALL_SCRIPTS)
|
||||||
|
|
||||||
def service_handler(service):
|
def service_handler(service):
|
||||||
"""Execute a service call to script.<script name>."""
|
"""Execute a service call to script.<script name>."""
|
||||||
@ -124,7 +126,7 @@ class ScriptEntity(ToggleEntity):
|
|||||||
def __init__(self, hass, object_id, name, sequence):
|
def __init__(self, hass, object_id, name, sequence):
|
||||||
"""Initialize the script."""
|
"""Initialize the script."""
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||||
self.script = Script(hass, sequence, name, self.update_ha_state)
|
self.script = Script(hass, sequence, name, self.async_update_ha_state)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
The arest sensor will consume an exposed aREST API of a device.
|
Support for an exposed aREST RESTful API of a device.
|
||||||
|
|
||||||
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.arest/
|
https://home-assistant.io/components/sensor.arest/
|
||||||
@ -8,30 +8,48 @@ import logging
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, DEVICE_DEFAULT_NAME,
|
CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_RESOURCE,
|
||||||
CONF_RESOURCE, CONF_MONITORED_VARIABLES)
|
CONF_MONITORED_VARIABLES, CONF_NAME, STATE_UNKNOWN)
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers import template
|
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||||
|
|
||||||
|
CONF_FUNCTIONS = 'functions'
|
||||||
|
CONF_PINS = 'pins'
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'aREST sensor'
|
||||||
|
|
||||||
|
PIN_VARIABLE_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||||
|
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
|
})
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_RESOURCE): cv.url,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_PINS, default={}):
|
||||||
|
vol.Schema({cv.string: PIN_VARIABLE_SCHEMA}),
|
||||||
|
vol.Optional(CONF_MONITORED_VARIABLES, default={}):
|
||||||
|
vol.Schema({cv.string: PIN_VARIABLE_SCHEMA}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the aREST sensor."""
|
"""Setup the aREST sensor."""
|
||||||
resource = config.get(CONF_RESOURCE)
|
resource = config.get(CONF_RESOURCE)
|
||||||
var_conf = config.get(CONF_MONITORED_VARIABLES)
|
var_conf = config.get(CONF_MONITORED_VARIABLES)
|
||||||
pins = config.get('pins', None)
|
pins = config.get(CONF_PINS)
|
||||||
|
|
||||||
if resource is None:
|
|
||||||
_LOGGER.error('Not all required config keys present: %s',
|
|
||||||
CONF_RESOURCE)
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(resource, timeout=10).json()
|
response = requests.get(resource, timeout=10).json()
|
||||||
@ -52,11 +70,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if value_template is None:
|
if value_template is None:
|
||||||
return lambda value: value
|
return lambda value: value
|
||||||
|
|
||||||
value_template = template.Template(value_template, hass)
|
value_template.hass = hass
|
||||||
|
|
||||||
def _render(value):
|
def _render(value):
|
||||||
try:
|
try:
|
||||||
return value_template.render({'value': value})
|
return value_template.async_render({'value': value})
|
||||||
except TemplateError:
|
except TemplateError:
|
||||||
_LOGGER.exception('Error parsing value')
|
_LOGGER.exception('Error parsing value')
|
||||||
return value
|
return value
|
||||||
@ -66,33 +84,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
dev = []
|
dev = []
|
||||||
|
|
||||||
if var_conf is not None:
|
if var_conf is not None:
|
||||||
for variable in var_conf:
|
for variable, var_data in var_conf.items():
|
||||||
if variable['name'] not in response['variables']:
|
if variable not in response['variables']:
|
||||||
_LOGGER.error('Variable: "%s" does not exist',
|
_LOGGER.error("Variable: '%s' does not exist", variable)
|
||||||
variable['name'])
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
renderer = make_renderer(variable.get(CONF_VALUE_TEMPLATE))
|
renderer = make_renderer(var_data.get(CONF_VALUE_TEMPLATE))
|
||||||
dev.append(ArestSensor(arest,
|
dev.append(ArestSensor(
|
||||||
resource,
|
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
||||||
config.get('name', response['name']),
|
variable, variable=variable,
|
||||||
variable['name'],
|
unit_of_measurement=var_data.get(CONF_UNIT_OF_MEASUREMENT),
|
||||||
variable=variable['name'],
|
renderer=renderer))
|
||||||
unit_of_measurement=variable.get(
|
|
||||||
ATTR_UNIT_OF_MEASUREMENT),
|
|
||||||
renderer=renderer))
|
|
||||||
|
|
||||||
if pins is not None:
|
if pins is not None:
|
||||||
for pinnum, pin in pins.items():
|
for pinnum, pin in pins.items():
|
||||||
renderer = make_renderer(pin.get(CONF_VALUE_TEMPLATE))
|
renderer = make_renderer(pin.get(CONF_VALUE_TEMPLATE))
|
||||||
dev.append(ArestSensor(ArestData(resource, pinnum),
|
dev.append(ArestSensor(
|
||||||
resource,
|
ArestData(resource, pinnum), resource,
|
||||||
config.get('name', response['name']),
|
config.get(CONF_NAME, response[CONF_NAME]), pin.get(CONF_NAME),
|
||||||
pin.get('name'),
|
pin=pinnum, unit_of_measurement=pin.get(
|
||||||
pin=pinnum,
|
CONF_UNIT_OF_MEASUREMENT), renderer=renderer))
|
||||||
unit_of_measurement=pin.get(
|
|
||||||
ATTR_UNIT_OF_MEASUREMENT),
|
|
||||||
renderer=renderer))
|
|
||||||
|
|
||||||
add_devices(dev)
|
add_devices(dev)
|
||||||
|
|
||||||
@ -106,18 +117,17 @@ class ArestSensor(Entity):
|
|||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self.arest = arest
|
self.arest = arest
|
||||||
self._resource = resource
|
self._resource = resource
|
||||||
self._name = '{} {}'.format(location.title(), name.title()) \
|
self._name = '{} {}'.format(location.title(), name.title())
|
||||||
or DEVICE_DEFAULT_NAME
|
|
||||||
self._variable = variable
|
self._variable = variable
|
||||||
self._pin = pin
|
self._pin = pin
|
||||||
self._state = 'n/a'
|
self._state = STATE_UNKNOWN
|
||||||
self._unit_of_measurement = unit_of_measurement
|
self._unit_of_measurement = unit_of_measurement
|
||||||
self._renderer = renderer
|
self._renderer = renderer
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
if self._pin is not None:
|
if self._pin is not None:
|
||||||
request = requests.get('{}/mode/{}/i'.format
|
request = requests.get(
|
||||||
(self._resource, self._pin), timeout=10)
|
'{}/mode/{}/i'.format(self._resource, self._pin), timeout=10)
|
||||||
if request.status_code is not 200:
|
if request.status_code is not 200:
|
||||||
_LOGGER.error("Can't set mode. Is device offline?")
|
_LOGGER.error("Can't set mode. Is device offline?")
|
||||||
|
|
||||||
@ -139,15 +149,19 @@ class ArestSensor(Entity):
|
|||||||
if 'error' in values:
|
if 'error' in values:
|
||||||
return values['error']
|
return values['error']
|
||||||
|
|
||||||
value = self._renderer(values.get('value',
|
value = self._renderer(
|
||||||
values.get(self._variable,
|
values.get('value', values.get(self._variable, STATE_UNKNOWN)))
|
||||||
'N/A')))
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest data from aREST API."""
|
"""Get the latest data from aREST API."""
|
||||||
self.arest.update()
|
self.arest.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Could the device be accessed during the last update call."""
|
||||||
|
return self.arest.available
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
class ArestData(object):
|
class ArestData(object):
|
||||||
@ -158,6 +172,7 @@ class ArestData(object):
|
|||||||
self._resource = resource
|
self._resource = resource
|
||||||
self._pin = pin
|
self._pin = pin
|
||||||
self.data = {}
|
self.data = {}
|
||||||
|
self.available = True
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
def update(self):
|
def update(self):
|
||||||
@ -179,7 +194,8 @@ class ArestData(object):
|
|||||||
response = requests.get('{}/digital/{}'.format(
|
response = requests.get('{}/digital/{}'.format(
|
||||||
self._resource, self._pin), timeout=10)
|
self._resource, self._pin), timeout=10)
|
||||||
self.data = {'value': response.json()['return_value']}
|
self.data = {'value': response.json()['return_value']}
|
||||||
|
self.available = True
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
_LOGGER.error("No route to device %s. Is device offline?",
|
_LOGGER.error("No route to device %s. Is device offline?",
|
||||||
self._resource)
|
self._resource)
|
||||||
self.data = {'error': 'error fetching'}
|
self.available = False
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Support for Forecast.io weather service.
|
Support for Dark Sky weather service.
|
||||||
|
|
||||||
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.forecast/
|
https://home-assistant.io/components/sensor.darksky/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@ -18,15 +18,14 @@ from homeassistant.helpers.entity import Entity
|
|||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['python-forecastio==1.3.4']
|
REQUIREMENTS = ['python-forecastio==1.3.5']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_UNITS = 'units'
|
CONF_UNITS = 'units'
|
||||||
|
CONF_UPDATE_INTERVAL = 'update_interval'
|
||||||
|
|
||||||
DEFAULT_NAME = 'Forecast.io'
|
DEFAULT_NAME = 'Dark Sky'
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
|
||||||
|
|
||||||
# Sensor types are defined like so:
|
# Sensor types are defined like so:
|
||||||
# Name, si unit, us unit, ca unit, uk unit, uk2 unit
|
# Name, si unit, us unit, ca unit, uk unit, uk2 unit
|
||||||
@ -84,12 +83,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||||
vol.Required(CONF_API_KEY): cv.string,
|
vol.Required(CONF_API_KEY): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2'])
|
vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']),
|
||||||
|
vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=120)): (
|
||||||
|
vol.All(cv.time_period, cv.positive_timedelta)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Forecast.io sensor."""
|
"""Setup the Dark Sky sensor."""
|
||||||
# Validate the configuration
|
# Validate the configuration
|
||||||
if None in (hass.config.latitude, hass.config.longitude):
|
if None in (hass.config.latitude, hass.config.longitude):
|
||||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||||
@ -105,9 +107,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
# Create a data fetcher to support all of the configured sensors. Then make
|
# Create a data fetcher to support all of the configured sensors. Then make
|
||||||
# the first call to init the data and confirm we can connect.
|
# the first call to init the data and confirm we can connect.
|
||||||
try:
|
try:
|
||||||
forecast_data = ForeCastData(
|
forecast_data = DarkSkyData(
|
||||||
config.get(CONF_API_KEY, None), hass.config.latitude,
|
api_key=config.get(CONF_API_KEY, None),
|
||||||
hass.config.longitude, units)
|
latitude=hass.config.latitude,
|
||||||
|
longitude=hass.config.longitude,
|
||||||
|
units=units,
|
||||||
|
interval=config.get(CONF_UPDATE_INTERVAL))
|
||||||
forecast_data.update_currently()
|
forecast_data.update_currently()
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
_LOGGER.error(error)
|
_LOGGER.error(error)
|
||||||
@ -117,14 +122,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
sensors = []
|
sensors = []
|
||||||
for variable in config[CONF_MONITORED_CONDITIONS]:
|
for variable in config[CONF_MONITORED_CONDITIONS]:
|
||||||
sensors.append(ForeCastSensor(forecast_data, variable, name))
|
sensors.append(DarkSkySensor(forecast_data, variable, name))
|
||||||
|
|
||||||
add_devices(sensors)
|
add_devices(sensors)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
class ForeCastSensor(Entity):
|
class DarkSkySensor(Entity):
|
||||||
"""Implementation of a Forecast.io sensor."""
|
"""Implementation of a Dark Sky sensor."""
|
||||||
|
|
||||||
def __init__(self, forecast_data, sensor_type, name):
|
def __init__(self, forecast_data, sensor_type, name):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
@ -175,10 +180,10 @@ class ForeCastSensor(Entity):
|
|||||||
|
|
||||||
# pylint: disable=too-many-branches,too-many-statements
|
# pylint: disable=too-many-branches,too-many-statements
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest data from Forecast.io and updates the states."""
|
"""Get the latest data from Dark Sky and updates the states."""
|
||||||
# Call the API for new forecast data. Each sensor will re-trigger this
|
# Call the API for new forecast data. Each sensor will re-trigger this
|
||||||
# same exact call, but thats fine. We cache results for a short period
|
# same exact call, but that's fine. We cache results for a short period
|
||||||
# of time to prevent hitting API limits. Note that forecast.io will
|
# of time to prevent hitting API limits. Note that Dark Sky will
|
||||||
# charge users for too many calls in 1 day, so take care when updating.
|
# charge users for too many calls in 1 day, so take care when updating.
|
||||||
self.forecast_data.update()
|
self.forecast_data.update()
|
||||||
self.update_unit_of_measurement()
|
self.update_unit_of_measurement()
|
||||||
@ -192,7 +197,8 @@ class ForeCastSensor(Entity):
|
|||||||
hourly = self.forecast_data.data_hourly
|
hourly = self.forecast_data.data_hourly
|
||||||
self._state = getattr(hourly, 'summary', '')
|
self._state = getattr(hourly, 'summary', '')
|
||||||
elif self.type in ['daily_summary',
|
elif self.type in ['daily_summary',
|
||||||
'temperature_min', 'temperature_max',
|
'temperature_min',
|
||||||
|
'temperature_max',
|
||||||
'apparent_temperature_min',
|
'apparent_temperature_min',
|
||||||
'apparent_temperature_max',
|
'apparent_temperature_max',
|
||||||
'precip_intensity_max']:
|
'precip_intensity_max']:
|
||||||
@ -242,12 +248,11 @@ def convert_to_camel(data):
|
|||||||
return components[0] + "".join(x.title() for x in components[1:])
|
return components[0] + "".join(x.title() for x in components[1:])
|
||||||
|
|
||||||
|
|
||||||
class ForeCastData(object):
|
class DarkSkyData(object):
|
||||||
"""Gets the latest data from Forecast.io."""
|
"""Get the latest data from Darksky."""
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
def __init__(self, api_key, latitude, longitude, units, interval):
|
||||||
def __init__(self, api_key, latitude, longitude, units):
|
|
||||||
"""Initialize the data object."""
|
"""Initialize the data object."""
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self.latitude = latitude
|
self.latitude = latitude
|
||||||
@ -261,36 +266,38 @@ class ForeCastData(object):
|
|||||||
self.data_hourly = None
|
self.data_hourly = None
|
||||||
self.data_daily = None
|
self.data_daily = None
|
||||||
|
|
||||||
|
# Apply throttling to methods using configured interval
|
||||||
|
self.update = Throttle(interval)(self._update)
|
||||||
|
self.update_currently = Throttle(interval)(self._update_currently)
|
||||||
|
self.update_minutely = Throttle(interval)(self._update_minutely)
|
||||||
|
self.update_hourly = Throttle(interval)(self._update_hourly)
|
||||||
|
self.update_daily = Throttle(interval)(self._update_daily)
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
def _update(self):
|
||||||
def update(self):
|
"""Get the latest data from Dark Sky."""
|
||||||
"""Get the latest data from Forecast.io."""
|
|
||||||
import forecastio
|
import forecastio
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.data = forecastio.load_forecast(
|
self.data = forecastio.load_forecast(
|
||||||
self._api_key, self.latitude, self.longitude, units=self.units)
|
self._api_key, self.latitude, self.longitude, units=self.units)
|
||||||
except (ConnectError, HTTPError, Timeout, ValueError) as error:
|
except (ConnectError, HTTPError, Timeout, ValueError) as error:
|
||||||
raise ValueError("Unable to init Forecast.io. - %s", error)
|
raise ValueError("Unable to init Dark Sky. %s", error)
|
||||||
self.unit_system = self.data.json['flags']['units']
|
self.unit_system = self.data.json['flags']['units']
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
def _update_currently(self):
|
||||||
def update_currently(self):
|
|
||||||
"""Update currently data."""
|
"""Update currently data."""
|
||||||
self.data_currently = self.data.currently()
|
self.data_currently = self.data.currently()
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
def _update_minutely(self):
|
||||||
def update_minutely(self):
|
|
||||||
"""Update minutely data."""
|
"""Update minutely data."""
|
||||||
self.data_minutely = self.data.minutely()
|
self.data_minutely = self.data.minutely()
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
def _update_hourly(self):
|
||||||
def update_hourly(self):
|
|
||||||
"""Update hourly data."""
|
"""Update hourly data."""
|
||||||
self.data_hourly = self.data.hourly()
|
self.data_hourly = self.data.hourly()
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
def _update_daily(self):
|
||||||
def update_daily(self):
|
|
||||||
"""Update daily data."""
|
"""Update daily data."""
|
||||||
self.data_daily = self.data.daily()
|
self.data_daily = self.data.daily()
|
@ -202,8 +202,7 @@ class EmonCmsData(object):
|
|||||||
"""Get the latest data."""
|
"""Get the latest data."""
|
||||||
try:
|
try:
|
||||||
req = requests.get(self._url, params={"apikey": self._apikey},
|
req = requests.get(self._url, params={"apikey": self._apikey},
|
||||||
verify=False, allow_redirects=True,
|
allow_redirects=True, timeout=5)
|
||||||
timeout=5)
|
|
||||||
except requests.exceptions.RequestException as exception:
|
except requests.exceptions.RequestException as exception:
|
||||||
_LOGGER.error(exception)
|
_LOGGER.error(exception)
|
||||||
return
|
return
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Support gahtering system information of hosts which are running glances.
|
Support gathering system information of hosts which are running glances.
|
||||||
|
|
||||||
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.glances/
|
https://home-assistant.io/components/sensor.glances/
|
||||||
@ -24,6 +24,8 @@ DEFAULT_HOST = 'localhost'
|
|||||||
DEFAULT_NAME = 'Glances'
|
DEFAULT_NAME = 'Glances'
|
||||||
DEFAULT_PORT = '61208'
|
DEFAULT_PORT = '61208'
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
'disk_use_percent': ['Disk Use', '%'],
|
'disk_use_percent': ['Disk Use', '%'],
|
||||||
'disk_use': ['Disk Use', 'GiB'],
|
'disk_use': ['Disk Use', 'GiB'],
|
||||||
@ -34,11 +36,11 @@ SENSOR_TYPES = {
|
|||||||
'swap_use_percent': ['Swap Use', '%'],
|
'swap_use_percent': ['Swap Use', '%'],
|
||||||
'swap_use': ['Swap Use', 'GiB'],
|
'swap_use': ['Swap Use', 'GiB'],
|
||||||
'swap_free': ['Swap Free', 'GiB'],
|
'swap_free': ['Swap Free', 'GiB'],
|
||||||
'processor_load': ['CPU Load', None],
|
'processor_load': ['CPU Load', '15 min'],
|
||||||
'process_running': ['Running', None],
|
'process_running': ['Running', 'Count'],
|
||||||
'process_total': ['Total', None],
|
'process_total': ['Total', 'Count'],
|
||||||
'process_thread': ['Thread', None],
|
'process_thread': ['Thread', 'Count'],
|
||||||
'process_sleeping': ['Sleeping', None]
|
'process_sleeping': ['Sleeping', 'Count']
|
||||||
}
|
}
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
@ -50,10 +52,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# Return cached results if last scan was less then this time ago.
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Glances sensor."""
|
"""Setup the Glances sensor."""
|
||||||
@ -66,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
try:
|
try:
|
||||||
response = requests.get(url, timeout=10)
|
response = requests.get(url, timeout=10)
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
_LOGGER.error('Response status is "%s"', response.status_code)
|
_LOGGER.error("Response status is '%s'", response.status_code)
|
||||||
return False
|
return False
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
_LOGGER.error("No route to resource/endpoint: %s", url)
|
_LOGGER.error("No route to resource/endpoint: %s", url)
|
||||||
|
@ -36,14 +36,14 @@ TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
|
|||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_ORIGIN): cv.string,
|
vol.Required(CONF_ORIGIN): cv.string,
|
||||||
vol.Required(CONF_DESTINATION): cv.string,
|
vol.Required(CONF_DESTINATION): cv.string,
|
||||||
vol.Required(CONF_DATA): cv.isfile,
|
vol.Required(CONF_DATA): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-locals
|
# pylint: disable=too-many-locals
|
||||||
def get_next_departure(sched, start_station_id, end_station_id):
|
def get_next_departure(sched, start_station_id, end_station_id):
|
||||||
"""Get the next departure for the given sched."""
|
"""Get the next departure for the given schedule."""
|
||||||
origin_station = sched.stops_by_id(start_station_id)[0]
|
origin_station = sched.stops_by_id(start_station_id)[0]
|
||||||
destination_station = sched.stops_by_id(end_station_id)[0]
|
destination_station = sched.stops_by_id(end_station_id)[0]
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ def get_next_departure(sched, start_station_id, end_station_id):
|
|||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Get the GTFS sensor."""
|
"""Set up the GTFS sensor."""
|
||||||
gtfs_dir = hass.config.path(DEFAULT_PATH)
|
gtfs_dir = hass.config.path(DEFAULT_PATH)
|
||||||
data = config.get(CONF_DATA)
|
data = config.get(CONF_DATA)
|
||||||
origin = config.get(CONF_ORIGIN)
|
origin = config.get(CONF_ORIGIN)
|
||||||
|
@ -18,7 +18,8 @@ DEPENDENCIES = ['homematic']
|
|||||||
|
|
||||||
HM_STATE_HA_CAST = {
|
HM_STATE_HA_CAST = {
|
||||||
"RotaryHandleSensor": {0: "closed", 1: "tilted", 2: "open"},
|
"RotaryHandleSensor": {0: "closed", 1: "tilted", 2: "open"},
|
||||||
"WaterSensor": {0: "dry", 1: "wet", 2: "water"}
|
"WaterSensor": {0: "dry", 1: "wet", 2: "water"},
|
||||||
|
"CO2Sensor": {0: "normal", 1: "added", 2: "strong"},
|
||||||
}
|
}
|
||||||
|
|
||||||
HM_UNIT_HA_CAST = {
|
HM_UNIT_HA_CAST = {
|
||||||
@ -38,6 +39,7 @@ HM_UNIT_HA_CAST = {
|
|||||||
"WIND_DIRECTION_RANGE": "°",
|
"WIND_DIRECTION_RANGE": "°",
|
||||||
"SUNSHINEDURATION": "#",
|
"SUNSHINEDURATION": "#",
|
||||||
"AIR_PRESSURE": "hPa",
|
"AIR_PRESSURE": "hPa",
|
||||||
|
"FREQUENCY": "Hz",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ class EmailReader:
|
|||||||
self.connection.select()
|
self.connection.select()
|
||||||
|
|
||||||
if len(self._unread_ids) == 0:
|
if len(self._unread_ids) == 0:
|
||||||
search = "SINCE {0:%d-%b-%y}".format(datetime.date.today())
|
search = "SINCE {0:%d-%b-%Y}".format(datetime.date.today())
|
||||||
if self._last_id is not None:
|
if self._last_id is not None:
|
||||||
search = "UID {}:*".format(self._last_id)
|
search = "UID {}:*".format(self._last_id)
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['pyloopenergy==0.0.14']
|
REQUIREMENTS = ['pyloopenergy==0.0.15']
|
||||||
|
|
||||||
CONF_ELEC = 'electricity'
|
CONF_ELEC = 'electricity'
|
||||||
CONF_GAS = 'gas'
|
CONF_GAS = 'gas'
|
||||||
|
@ -11,7 +11,6 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DEPENDENCIES = []
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
@ -66,6 +65,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
})
|
})
|
||||||
map_sv_types[pres.S_LIGHT_LEVEL].append(set_req.V_LEVEL)
|
map_sv_types[pres.S_LIGHT_LEVEL].append(set_req.V_LEVEL)
|
||||||
|
|
||||||
|
if float(gateway.protocol_version) >= 2.0:
|
||||||
|
map_sv_types.update({
|
||||||
|
pres.S_INFO: [set_req.V_TEXT],
|
||||||
|
pres.S_GAS: [set_req.V_FLOW, set_req.V_VOLUME],
|
||||||
|
pres.S_GPS: [set_req.V_POSITION],
|
||||||
|
pres.S_WATER_QUALITY: [set_req.V_TEMP, set_req.V_PH,
|
||||||
|
set_req.V_ORP, set_req.V_EC]
|
||||||
|
})
|
||||||
|
map_sv_types[pres.S_CUSTOM].append(set_req.V_CUSTOM)
|
||||||
|
map_sv_types[pres.S_POWER].extend(
|
||||||
|
[set_req.V_VAR, set_req.V_VA, set_req.V_POWER_FACTOR])
|
||||||
|
|
||||||
devices = {}
|
devices = {}
|
||||||
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
|
||||||
map_sv_types, devices, add_devices, MySensorsSensor))
|
map_sv_types, devices, add_devices, MySensorsSensor))
|
||||||
@ -74,6 +85,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity):
|
class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity):
|
||||||
"""Representation of a MySensors Sensor child node."""
|
"""Representation of a MySensors Sensor child node."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def force_update(self):
|
||||||
|
"""Return True if state updates should be forced.
|
||||||
|
|
||||||
|
If True, a state change will be triggered anytime the state property is
|
||||||
|
updated, not just when the value changes.
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
@ -104,4 +124,11 @@ class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity):
|
|||||||
return self._values[
|
return self._values[
|
||||||
set_req.V_UNIT_PREFIX]
|
set_req.V_UNIT_PREFIX]
|
||||||
unit_map.update({set_req.V_PERCENTAGE: '%'})
|
unit_map.update({set_req.V_PERCENTAGE: '%'})
|
||||||
|
if float(self.gateway.protocol_version) >= 2.0:
|
||||||
|
unit_map.update({
|
||||||
|
set_req.V_ORP: 'mV',
|
||||||
|
set_req.V_EC: 'μS/cm',
|
||||||
|
set_req.V_VAR: 'var',
|
||||||
|
set_req.V_VA: 'VA',
|
||||||
|
})
|
||||||
return unit_map.get(self.value_type)
|
return unit_map.get(self.value_type)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user