mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
commit
ef2ed7bfc9
22
.coveragerc
22
.coveragerc
@ -31,6 +31,9 @@ omit =
|
||||
homeassistant/components/insteon_hub.py
|
||||
homeassistant/components/*/insteon_hub.py
|
||||
|
||||
homeassistant/components/ios.py
|
||||
homeassistant/components/*/ios.py
|
||||
|
||||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
@ -95,8 +98,7 @@ omit =
|
||||
homeassistant/components/homematic.py
|
||||
homeassistant/components/*/homematic.py
|
||||
|
||||
homeassistant/components/pilight.py
|
||||
homeassistant/components/*/pilight.py
|
||||
homeassistant/components/switch/pilight.py
|
||||
|
||||
homeassistant/components/knx.py
|
||||
homeassistant/components/*/knx.py
|
||||
@ -104,16 +106,22 @@ omit =
|
||||
homeassistant/components/ffmpeg.py
|
||||
homeassistant/components/*/ffmpeg.py
|
||||
|
||||
homeassistant/components/zoneminder.py
|
||||
homeassistant/components/*/zoneminder.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
homeassistant/components/binary_sensor/concord232.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/synology.py
|
||||
homeassistant/components/climate/eq3btsmart.py
|
||||
homeassistant/components/climate/heatmiser.py
|
||||
homeassistant/components/climate/homematic.py
|
||||
@ -127,6 +135,7 @@ omit =
|
||||
homeassistant/components/device_tracker/actiontec.py
|
||||
homeassistant/components/device_tracker/aruba.py
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
homeassistant/components/device_tracker/bbox.py
|
||||
homeassistant/components/device_tracker/bluetooth_tracker.py
|
||||
homeassistant/components/device_tracker/bluetooth_le_tracker.py
|
||||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||
@ -144,6 +153,7 @@ omit =
|
||||
homeassistant/components/device_tracker/volvooncall.py
|
||||
homeassistant/components/discovery.py
|
||||
homeassistant/components/downloader.py
|
||||
homeassistant/components/emoncms_history.py
|
||||
homeassistant/components/fan/mqtt.py
|
||||
homeassistant/components/feedreader.py
|
||||
homeassistant/components/foursquare.py
|
||||
@ -197,6 +207,7 @@ omit =
|
||||
homeassistant/components/notify/joaoapps_join.py
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/matrix.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/pushbullet.py
|
||||
@ -209,6 +220,7 @@ omit =
|
||||
homeassistant/components/notify/smtp.py
|
||||
homeassistant/components/notify/syslog.py
|
||||
homeassistant/components/notify/telegram.py
|
||||
homeassistant/components/notify/telstra.py
|
||||
homeassistant/components/notify/twilio_sms.py
|
||||
homeassistant/components/notify/twitter.py
|
||||
homeassistant/components/notify/xmpp.py
|
||||
@ -216,6 +228,8 @@ omit =
|
||||
homeassistant/components/openalpr.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
homeassistant/components/sensor/bitcoin.py
|
||||
homeassistant/components/sensor/bom.py
|
||||
homeassistant/components/sensor/coinmarketcap.py
|
||||
@ -235,6 +249,7 @@ omit =
|
||||
homeassistant/components/sensor/google_travel_time.py
|
||||
homeassistant/components/sensor/gpsd.py
|
||||
homeassistant/components/sensor/gtfs.py
|
||||
homeassistant/components/sensor/haveibeenpwned.py
|
||||
homeassistant/components/sensor/hp_ilo.py
|
||||
homeassistant/components/sensor/imap.py
|
||||
homeassistant/components/sensor/imap_email_content.py
|
||||
@ -254,6 +269,7 @@ omit =
|
||||
homeassistant/components/sensor/plex.py
|
||||
homeassistant/components/sensor/rest.py
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
@ -272,7 +288,6 @@ omit =
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/worldclock.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yahoo_finance.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
@ -281,6 +296,7 @@ omit =
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/neato.py
|
||||
homeassistant/components/switch/netio.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
|
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,9 +1,9 @@
|
||||
**Description:**
|
||||
|
||||
|
||||
**Related issue (if applicable):** fixes #
|
||||
**Related issue (if applicable):** fixes #<home-assistant issue number goes here>
|
||||
|
||||
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#
|
||||
**Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>
|
||||
|
||||
**Example entry for `configuration.yaml` (if applicable):**
|
||||
```yaml
|
||||
@ -13,7 +13,7 @@
|
||||
**Checklist:**
|
||||
|
||||
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.github.io](https://github.com/home-assistant/home-assistant.github.io)
|
||||
|
||||
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**
|
||||
|
@ -19,8 +19,7 @@ RUN script/build_python_openzwave && \
|
||||
ln -sf /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/python-openzwave/config
|
||||
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
# certifi breaks Debian based installs
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi && \
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && \
|
||||
pip3 install mysqlclient psycopg2 uvloop
|
||||
|
||||
# Copy source
|
||||
|
@ -8,11 +8,31 @@
|
||||
.. autoclass:: Config
|
||||
:members:
|
||||
|
||||
.. autoclass:: Event
|
||||
:members:
|
||||
|
||||
.. autoclass:: EventBus
|
||||
:members:
|
||||
|
||||
.. autoclass:: HomeAssistant
|
||||
:members:
|
||||
|
||||
.. autoclass:: State
|
||||
:members:
|
||||
|
||||
.. autoclass:: StateMachine
|
||||
:members:
|
||||
|
||||
.. autoclass:: ServiceCall
|
||||
:members:
|
||||
|
||||
.. autoclass:: ServiceRegistry
|
||||
:members:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: homeassistant.core
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
@ -4,6 +4,14 @@ homeassistant.util package
|
||||
Submodules
|
||||
----------
|
||||
|
||||
homeassistant.util.async module
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: homeassistant.util.async
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
homeassistant.util.color module
|
||||
-------------------------------
|
||||
|
||||
|
@ -2,7 +2,7 @@ swagger: '2.0'
|
||||
info:
|
||||
title: Home Assistant
|
||||
description: Home Assistant REST API
|
||||
version: "1.0.0"
|
||||
version: "1.0.1"
|
||||
# the domain of the service
|
||||
host: localhost:8123
|
||||
|
||||
@ -12,17 +12,17 @@ schemes:
|
||||
- https
|
||||
|
||||
securityDefinitions:
|
||||
api_key:
|
||||
type: apiKey
|
||||
description: API password
|
||||
name: api_password
|
||||
in: query
|
||||
|
||||
# api_key:
|
||||
#api_key:
|
||||
# type: apiKey
|
||||
# description: API password
|
||||
# name: x-ha-access
|
||||
# in: header
|
||||
# name: api_password
|
||||
# in: query
|
||||
|
||||
api_key:
|
||||
type: apiKey
|
||||
description: API password
|
||||
name: x-ha-access
|
||||
in: header
|
||||
|
||||
# will be prefixed to all paths
|
||||
basePath: /api
|
||||
@ -38,6 +38,8 @@ paths:
|
||||
description: Returns message if API is up and running.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: API is up and running
|
||||
@ -53,6 +55,8 @@ paths:
|
||||
description: Returns the current configuration as JSON.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Current configuration
|
||||
@ -81,6 +85,8 @@ paths:
|
||||
summary: Returns all data needed to bootstrap Home Assistant.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Bootstrap information
|
||||
@ -96,6 +102,8 @@ paths:
|
||||
description: Returns an array of event objects. Each event object contain event name and listener count.
|
||||
tags:
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Events
|
||||
@ -113,6 +121,8 @@ paths:
|
||||
description: Returns an array of service objects. Each object contains the domain and which services it contains.
|
||||
tags:
|
||||
- Services
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: Services
|
||||
@ -130,6 +140,8 @@ paths:
|
||||
description: Returns an array of state changes in the past. Each object contains further detail for the entities.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: State changes
|
||||
@ -148,6 +160,8 @@ paths:
|
||||
Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
responses:
|
||||
200:
|
||||
description: States
|
||||
@ -166,6 +180,8 @@ paths:
|
||||
Returns a state object for specified entity_id.
|
||||
tags:
|
||||
- State
|
||||
security:
|
||||
- api_key: []
|
||||
parameters:
|
||||
- name: entity_id
|
||||
in: path
|
||||
@ -223,6 +239,8 @@ paths:
|
||||
Retrieve all errors logged during the current session of Home Assistant as a plaintext response.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
@ -239,6 +257,8 @@ paths:
|
||||
Returns the data (image) from the specified camera entity_id.
|
||||
tags:
|
||||
- Camera
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- image/jpeg
|
||||
parameters:
|
||||
@ -262,6 +282,8 @@ paths:
|
||||
Fires an event with event_type
|
||||
tags:
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
@ -286,6 +308,8 @@ paths:
|
||||
Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first.
|
||||
tags:
|
||||
- Services
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
@ -317,6 +341,8 @@ paths:
|
||||
Render a Home Assistant template.
|
||||
tags:
|
||||
- Template
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
@ -338,6 +364,8 @@ paths:
|
||||
Setup event forwarding to another Home Assistant instance.
|
||||
tags:
|
||||
- Core
|
||||
security:
|
||||
- api_key: []
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
@ -376,6 +404,8 @@ paths:
|
||||
tags:
|
||||
- Core
|
||||
- Events
|
||||
security:
|
||||
- api_key: []
|
||||
produces:
|
||||
- text/event-stream
|
||||
parameters:
|
||||
@ -420,8 +450,16 @@ definitions:
|
||||
location_name:
|
||||
type: string
|
||||
unit_system:
|
||||
type: string
|
||||
description: The system for measurement units
|
||||
type: object
|
||||
properties:
|
||||
length:
|
||||
type: string
|
||||
mass:
|
||||
type: string
|
||||
temperature:
|
||||
type: string
|
||||
volume:
|
||||
type: string
|
||||
time_zone:
|
||||
type: string
|
||||
version:
|
||||
|
@ -32,12 +32,15 @@ _CURRENT_SETUP = []
|
||||
ATTR_COMPONENT = 'component'
|
||||
|
||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
_PERSISTENT_PLATFORMS = set()
|
||||
_PERSISTENT_VALIDATION = set()
|
||||
|
||||
|
||||
def setup_component(hass: core.HomeAssistant, domain: str,
|
||||
config: Optional[Dict]=None) -> bool:
|
||||
"""Setup a component and all its dependencies."""
|
||||
if domain in hass.config.components:
|
||||
_LOGGER.debug('Component %s already set up.', domain)
|
||||
return True
|
||||
|
||||
_ensure_loader_prepared(hass)
|
||||
@ -53,6 +56,7 @@ def setup_component(hass: core.HomeAssistant, domain: str,
|
||||
|
||||
for component in components:
|
||||
if not _setup_component(hass, component, config):
|
||||
_LOGGER.error('Component %s failed to setup', component)
|
||||
return False
|
||||
|
||||
return True
|
||||
@ -147,7 +151,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
try:
|
||||
config = component.CONFIG_SCHEMA(config)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, domain, config)
|
||||
log_exception(ex, domain, config, hass)
|
||||
return None
|
||||
|
||||
elif hasattr(component, 'PLATFORM_SCHEMA'):
|
||||
@ -157,8 +161,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
try:
|
||||
p_validated = component.PLATFORM_SCHEMA(p_config)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, domain, config)
|
||||
return None
|
||||
log_exception(ex, domain, config, hass)
|
||||
continue
|
||||
|
||||
# Not all platform components follow same pattern for platforms
|
||||
# So if p_name is None we are not going to validate platform
|
||||
@ -171,7 +175,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
p_name)
|
||||
|
||||
if platform is None:
|
||||
return None
|
||||
continue
|
||||
|
||||
# Validate platform specific schema
|
||||
if hasattr(platform, 'PLATFORM_SCHEMA'):
|
||||
@ -179,8 +183,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
|
||||
p_validated = platform.PLATFORM_SCHEMA(p_validated)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, '{}.{}'.format(domain, p_name),
|
||||
p_validated)
|
||||
return None
|
||||
p_validated, hass)
|
||||
continue
|
||||
|
||||
platforms.append(p_validated)
|
||||
|
||||
@ -209,6 +213,13 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
|
||||
# Not found
|
||||
if platform is None:
|
||||
_LOGGER.error('Unable to find platform %s', platform_path)
|
||||
|
||||
_PERSISTENT_PLATFORMS.add(platform_path)
|
||||
message = ('Unable to find the following platforms: ' +
|
||||
', '.join(list(_PERSISTENT_PLATFORMS)) +
|
||||
'(please check your configuration)')
|
||||
persistent_notification.create(
|
||||
hass, message, 'Invalid platforms', 'platform_errors')
|
||||
return None
|
||||
|
||||
# Already loaded
|
||||
@ -255,7 +266,7 @@ def from_config_dict(config: Dict[str, Any],
|
||||
try:
|
||||
conf_util.process_ha_core_config(hass, core_config)
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, 'homeassistant', core_config)
|
||||
log_exception(ex, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
|
||||
conf_util.process_ha_config_upgrade(hass)
|
||||
@ -303,6 +314,7 @@ def from_config_dict(config: Dict[str, Any],
|
||||
hass.loop.run_until_complete(
|
||||
hass.loop.run_in_executor(None, component_setup)
|
||||
)
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
@ -343,6 +355,11 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
"[%(name)s] %(message)s%(reset)s")
|
||||
|
||||
# suppress overly verbose logs from libraries that aren't helpful
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
|
||||
try:
|
||||
from colorlog import ColoredFormatter
|
||||
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
||||
@ -395,9 +412,16 @@ def _ensure_loader_prepared(hass: core.HomeAssistant) -> None:
|
||||
loader.prepare(hass)
|
||||
|
||||
|
||||
def log_exception(ex, domain, config):
|
||||
def log_exception(ex, domain, config, hass=None):
|
||||
"""Generate log exception for config validation."""
|
||||
message = 'Invalid config for [{}]: '.format(domain)
|
||||
if hass is not None:
|
||||
_PERSISTENT_VALIDATION.add(domain)
|
||||
message = ('The following platforms contain invalid configuration: ' +
|
||||
', '.join(list(_PERSISTENT_VALIDATION)) +
|
||||
' (please check your configuration)')
|
||||
persistent_notification.create(
|
||||
hass, message, 'Invalid config', 'invalid_config')
|
||||
|
||||
if 'extra keys not allowed' in ex.error_message:
|
||||
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
|
||||
|
136
homeassistant/components/alarm_control_panel/concord232.py
Executable file
136
homeassistant/components/alarm_control_panel/concord232.py
Executable file
@ -0,0 +1,136 @@
|
||||
"""
|
||||
Support for Concord232 alarm control panels.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.concord232/
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import logging
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PORT,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
REQUIREMENTS = ['concord232==0.14']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'CONCORD232'
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
})
|
||||
|
||||
SCAN_INTERVAL = 1
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup concord232 platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
url = 'http://{}:{}'.format(host, port)
|
||||
|
||||
try:
|
||||
add_devices([Concord232Alarm(hass, url, name)])
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to Concord232: %s', str(ex))
|
||||
return False
|
||||
|
||||
|
||||
class Concord232Alarm(alarm.AlarmControlPanel):
|
||||
"""Represents the Concord232-based alarm panel."""
|
||||
|
||||
def __init__(self, hass, url, name):
|
||||
"""Initalize the concord232 alarm panel."""
|
||||
from concord232 import client as concord232_client
|
||||
|
||||
self._state = STATE_UNKNOWN
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._url = url
|
||||
|
||||
try:
|
||||
client = concord232_client.Client(self._url)
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to Concord232: %s', str(ex))
|
||||
|
||||
self._alarm = client
|
||||
self._alarm.partitions = self._alarm.list_partitions()
|
||||
self._alarm.last_partition_update = datetime.datetime.now()
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling needed."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""The characters if code is defined."""
|
||||
return '[0-9]{4}([0-9]{2})?'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update values from API."""
|
||||
try:
|
||||
part = self._alarm.list_partitions()[0]
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
|
||||
dict(host=self._url, reason=ex))
|
||||
newstate = STATE_UNKNOWN
|
||||
except IndexError:
|
||||
_LOGGER.error('concord232 reports no partitions')
|
||||
newstate = STATE_UNKNOWN
|
||||
|
||||
if part['arming_level'] == "Off":
|
||||
newstate = STATE_ALARM_DISARMED
|
||||
elif "Home" in part['arming_level']:
|
||||
newstate = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
newstate = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
if not newstate == self._state:
|
||||
_LOGGER.info("State Chnage from %s to %s", self._state, newstate)
|
||||
self._state = newstate
|
||||
return self._state
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._alarm.disarm(code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._alarm.arm('home')
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('auto')
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command."""
|
||||
raise NotImplementedError()
|
@ -60,6 +60,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
# talk to the API and trigger a requests exception for setup_platform()
|
||||
# to catch
|
||||
self._alarm.list_zones()
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -79,16 +80,20 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Process new events from panel."""
|
||||
try:
|
||||
part = self._alarm.list_partitions()[0]
|
||||
zones = self._alarm.list_zones()
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to %(host)s: %(reason)s',
|
||||
dict(host=self._url, reason=ex))
|
||||
return STATE_UNKNOWN
|
||||
self._state = STATE_UNKNOWN
|
||||
except IndexError:
|
||||
_LOGGER.error('nx584 reports no partitions')
|
||||
return STATE_UNKNOWN
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
bypassed = False
|
||||
for zone in zones:
|
||||
@ -100,11 +105,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||
break
|
||||
|
||||
if not part['armed']:
|
||||
return STATE_ALARM_DISARMED
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif bypassed:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
@ -7,16 +7,20 @@ https://home-assistant.io/components/alexa/
|
||||
import copy
|
||||
import enum
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import template, script, config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_ENDPOINT = '/api/alexa'
|
||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
||||
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/<briefing_id>'
|
||||
|
||||
CONF_ACTION = 'action'
|
||||
CONF_CARD = 'card'
|
||||
@ -28,6 +32,23 @@ CONF_TITLE = 'title'
|
||||
CONF_CONTENT = 'content'
|
||||
CONF_TEXT = 'text'
|
||||
|
||||
CONF_FLASH_BRIEFINGS = 'flash_briefings'
|
||||
CONF_UID = 'uid'
|
||||
CONF_DATE = 'date'
|
||||
CONF_TITLE = 'title'
|
||||
CONF_AUDIO = 'audio'
|
||||
CONF_TEXT = 'text'
|
||||
CONF_DISPLAY_URL = 'display_url'
|
||||
|
||||
ATTR_UID = 'uid'
|
||||
ATTR_UPDATE_DATE = 'updateDate'
|
||||
ATTR_TITLE_TEXT = 'titleText'
|
||||
ATTR_STREAM_URL = 'streamUrl'
|
||||
ATTR_MAIN_TEXT = 'mainText'
|
||||
ATTR_REDIRECTION_URL = 'redirectionURL'
|
||||
|
||||
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
@ -61,6 +82,16 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TEXT): cv.template,
|
||||
}
|
||||
}
|
||||
},
|
||||
CONF_FLASH_BRIEFINGS: {
|
||||
cv.string: vol.All(cv.ensure_list, [{
|
||||
vol.Required(CONF_UID, default=str(uuid.uuid4())): cv.string,
|
||||
vol.Optional(CONF_DATE, default=datetime.utcnow()): cv.string,
|
||||
vol.Required(CONF_TITLE): cv.template,
|
||||
vol.Optional(CONF_AUDIO): cv.template,
|
||||
vol.Required(CONF_TEXT, default=""): cv.template,
|
||||
vol.Optional(CONF_DISPLAY_URL): cv.template,
|
||||
}]),
|
||||
}
|
||||
}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
@ -68,16 +99,19 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
|
||||
def setup(hass, config):
|
||||
"""Activate Alexa component."""
|
||||
hass.wsgi.register_view(AlexaView(hass,
|
||||
config[DOMAIN].get(CONF_INTENTS, {})))
|
||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
|
||||
|
||||
hass.wsgi.register_view(AlexaIntentsView(hass, intents))
|
||||
hass.wsgi.register_view(AlexaFlashBriefingView(hass, flash_briefings))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AlexaView(HomeAssistantView):
|
||||
class AlexaIntentsView(HomeAssistantView):
|
||||
"""Handle Alexa requests."""
|
||||
|
||||
url = API_ENDPOINT
|
||||
url = INTENTS_API_ENDPOINT
|
||||
name = 'api:alexa'
|
||||
|
||||
def __init__(self, hass, intents):
|
||||
@ -235,3 +269,69 @@ class AlexaResponse(object):
|
||||
'sessionAttributes': self.session_attributes,
|
||||
'response': response,
|
||||
}
|
||||
|
||||
|
||||
class AlexaFlashBriefingView(HomeAssistantView):
|
||||
"""Handle Alexa Flash Briefing skill requests."""
|
||||
|
||||
url = FLASH_BRIEFINGS_API_ENDPOINT
|
||||
name = 'api:alexa:flash_briefings'
|
||||
|
||||
def __init__(self, hass, flash_briefings):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__(hass)
|
||||
self.flash_briefings = copy.deepcopy(flash_briefings)
|
||||
template.attach(hass, self.flash_briefings)
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def get(self, request, briefing_id):
|
||||
"""Handle Alexa Flash Briefing request."""
|
||||
_LOGGER.debug('Received Alexa flash briefing request for: %s',
|
||||
briefing_id)
|
||||
|
||||
if self.flash_briefings.get(briefing_id) is None:
|
||||
err = 'No configured Alexa flash briefing was found for: %s'
|
||||
_LOGGER.error(err, briefing_id)
|
||||
return self.Response(status=404)
|
||||
|
||||
briefing = []
|
||||
|
||||
for item in self.flash_briefings.get(briefing_id, []):
|
||||
output = {}
|
||||
if item.get(CONF_TITLE) is not None:
|
||||
if isinstance(item.get(CONF_TITLE), template.Template):
|
||||
output[ATTR_TITLE_TEXT] = item[CONF_TITLE].render()
|
||||
else:
|
||||
output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE)
|
||||
|
||||
if item.get(CONF_TEXT) is not None:
|
||||
if isinstance(item.get(CONF_TEXT), template.Template):
|
||||
output[ATTR_MAIN_TEXT] = item[CONF_TEXT].render()
|
||||
else:
|
||||
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
|
||||
|
||||
if item.get(CONF_UID) is not None:
|
||||
output[ATTR_UID] = item.get(CONF_UID)
|
||||
|
||||
if item.get(CONF_AUDIO) is not None:
|
||||
if isinstance(item.get(CONF_AUDIO), template.Template):
|
||||
output[ATTR_STREAM_URL] = item[CONF_AUDIO].render()
|
||||
else:
|
||||
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
|
||||
|
||||
if item.get(CONF_DISPLAY_URL) is not None:
|
||||
if isinstance(item.get(CONF_DISPLAY_URL),
|
||||
template.Template):
|
||||
output[ATTR_REDIRECTION_URL] = \
|
||||
item[CONF_DISPLAY_URL].render()
|
||||
else:
|
||||
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
||||
|
||||
if isinstance(item[CONF_DATE], str):
|
||||
item[CONF_DATE] = dt_util.parse_datetime(item[CONF_DATE])
|
||||
|
||||
output[ATTR_UPDATE_DATE] = item[CONF_DATE].strftime(DATE_FORMAT)
|
||||
|
||||
briefing.append(output)
|
||||
|
||||
return self.json(briefing)
|
||||
|
@ -11,6 +11,7 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
@ -157,24 +158,24 @@ def setup(hass, config):
|
||||
descriptions = conf_util.load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
def trigger_service_handler(service_call):
|
||||
"""Handle automation triggers."""
|
||||
for entity in component.extract_from_service(service_call):
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
hass.loop.create_task(entity.async_trigger(
|
||||
service_call.data.get(ATTR_VARIABLES), True))
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
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.async_extract_from_service(service_call):
|
||||
hass.loop.create_task(getattr(entity, method)())
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
def toggle_service_handler(service_call):
|
||||
"""Handle automation toggle service calls."""
|
||||
for entity in component.extract_from_service(service_call):
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
if entity.is_on:
|
||||
hass.loop.create_task(entity.async_turn_off())
|
||||
else:
|
||||
@ -183,8 +184,7 @@ def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
"""Remove all automations and load new ones from config."""
|
||||
conf = yield from hass.loop.run_in_executor(
|
||||
None, component.prepare_reload)
|
||||
conf = yield from component.async_prepare_reload()
|
||||
if conf is None:
|
||||
return
|
||||
hass.loop.create_task(_async_process_config(hass, conf, component))
|
||||
@ -271,7 +271,9 @@ class AutomationEntity(ToggleEntity):
|
||||
self._async_detach_triggers()
|
||||
self._async_detach_triggers = None
|
||||
self._enabled = False
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
# It's important that the update is finished before this method
|
||||
# ends because async_remove depends on it.
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(self, variables, skip_condition=False):
|
||||
@ -280,15 +282,15 @@ class AutomationEntity(ToggleEntity):
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if skip_condition or self._cond_func(variables):
|
||||
yield from self._async_action(variables)
|
||||
yield from self._async_action(self.entity_id, variables)
|
||||
self._last_triggered = utcnow()
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
|
||||
def remove(self):
|
||||
@asyncio.coroutine
|
||||
def async_remove(self):
|
||||
"""Remove automation from HASS."""
|
||||
run_coroutine_threadsafe(self.async_turn_off(),
|
||||
self.hass.loop).result()
|
||||
super().remove()
|
||||
yield from self.async_turn_off()
|
||||
yield from super().async_remove()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_enable(self):
|
||||
@ -341,12 +343,11 @@ def _async_process_config(hass, config, component):
|
||||
entity = AutomationEntity(name, async_attach_triggers, cond_func,
|
||||
action, hidden)
|
||||
if config_block[CONF_INITIAL_STATE]:
|
||||
tasks.append(hass.loop.create_task(entity.async_enable()))
|
||||
tasks.append(entity.async_enable())
|
||||
entities.append(entity)
|
||||
|
||||
yield from asyncio.gather(*tasks, loop=hass.loop)
|
||||
yield from hass.loop.run_in_executor(
|
||||
None, component.add_entities, entities)
|
||||
hass.loop.create_task(component.async_add_entities(entities))
|
||||
|
||||
return len(entities) > 0
|
||||
|
||||
@ -356,10 +357,11 @@ def _async_get_action(hass, config, name):
|
||||
script_obj = script.Script(hass, config, name)
|
||||
|
||||
@asyncio.coroutine
|
||||
def action(variables=None):
|
||||
def action(entity_id, variables):
|
||||
"""Action to be executed."""
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.async_log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
logbook.async_log_entry(
|
||||
hass, name, 'has been triggered', DOMAIN, entity_id)
|
||||
hass.loop.create_task(script_obj.async_run(variables))
|
||||
|
||||
return action
|
||||
|
143
homeassistant/components/binary_sensor/concord232.py
Executable file
143
homeassistant/components/binary_sensor/concord232.py
Executable file
@ -0,0 +1,143 @@
|
||||
"""
|
||||
Support for exposing Concord232 elements as sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.concord232/
|
||||
"""
|
||||
import datetime
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES)
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
REQUIREMENTS = ['concord232==0.14']
|
||||
|
||||
_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,
|
||||
})
|
||||
|
||||
SCAN_INTERVAL = 1
|
||||
|
||||
DEFAULT_NAME = "Alarm"
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Concord232 binary sensor platform."""
|
||||
from concord232 import client as concord232_client
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
exclude = config.get(CONF_EXCLUDE_ZONES)
|
||||
zone_types = config.get(CONF_ZONE_TYPES)
|
||||
sensors = []
|
||||
|
||||
try:
|
||||
_LOGGER.debug('Initializing Client.')
|
||||
client = concord232_client.Client('http://{}:{}'
|
||||
.format(host, port))
|
||||
client.zones = client.list_zones()
|
||||
client.last_zone_update = datetime.datetime.now()
|
||||
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error('Unable to connect to Concord232: %s', str(ex))
|
||||
return False
|
||||
|
||||
for zone in client.zones:
|
||||
_LOGGER.info('Loading Zone found: %s', zone['name'])
|
||||
if zone['number'] not in exclude:
|
||||
sensors.append(Concord232ZoneSensor(
|
||||
hass,
|
||||
client,
|
||||
zone,
|
||||
zone_types.get(zone['number'], get_opening_type(zone))))
|
||||
|
||||
add_devices(sensors)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_opening_type(zone):
|
||||
"""Helper function to try to guess sensor type frm name."""
|
||||
if "MOTION" in zone["name"]:
|
||||
return "motion"
|
||||
if "KEY" in zone["name"]:
|
||||
return "safety"
|
||||
if "SMOKE" in zone["name"]:
|
||||
return "smoke"
|
||||
if "WATER" in zone["name"]:
|
||||
return "water"
|
||||
return "opening"
|
||||
|
||||
|
||||
class Concord232ZoneSensor(BinarySensorDevice):
|
||||
"""Representation of a Concord232 zone as a sensor."""
|
||||
|
||||
def __init__(self, hass, client, zone, zone_type):
|
||||
"""Initialize the Concord232 binary sensor."""
|
||||
self._hass = hass
|
||||
self._client = client
|
||||
self._zone = zone
|
||||
self._number = zone['number']
|
||||
self._zone_type = zone_type
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._zone['name']
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
# True means "faulted" or "open" or "abnormal state"
|
||||
return bool(self._zone['state'] == 'Normal')
|
||||
|
||||
def update(self):
|
||||
""""Get updated stats from API."""
|
||||
last_update = datetime.datetime.now() - self._client.last_zone_update
|
||||
_LOGGER.debug("Zone: %s ", self._zone)
|
||||
if last_update > datetime.timedelta(seconds=1):
|
||||
self._client.zones = self._client.list_zones()
|
||||
self._client.last_zone_update = datetime.datetime.now()
|
||||
_LOGGER.debug("Updated from Zone: %s", self._zone['name'])
|
||||
|
||||
if hasattr(self._client, 'zones'):
|
||||
self._zone = next((x for x in self._client.zones
|
||||
if x['number'] == self._number), None)
|
127
homeassistant/components/binary_sensor/netatmo.py
Normal file
127
homeassistant/components/binary_sensor/netatmo.py
Normal file
@ -0,0 +1,127 @@
|
||||
"""
|
||||
Support for the Netatmo binary sensors.
|
||||
|
||||
The binary sensors based on events seen by the NetatmoCamera
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.netatmo/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.netatmo import WelcomeData
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
DEPENDENCIES = ["netatmo"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# These are the available sensors mapped to binary_sensor class
|
||||
SENSOR_TYPES = {
|
||||
"Someone known": "motion",
|
||||
"Someone unknown": "motion",
|
||||
"Motion": "motion",
|
||||
}
|
||||
|
||||
CONF_HOME = 'home'
|
||||
CONF_CAMERAS = 'cameras'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME): cv.string,
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup access to Netatmo binary sensor."""
|
||||
netatmo = get_component('netatmo')
|
||||
home = config.get(CONF_HOME, None)
|
||||
|
||||
import lnetatmo
|
||||
try:
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
if data.get_camera_names() == []:
|
||||
return None
|
||||
except lnetatmo.NoDevice:
|
||||
return None
|
||||
|
||||
sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES)
|
||||
|
||||
for camera_name in data.get_camera_names():
|
||||
if CONF_CAMERAS in config:
|
||||
if config[CONF_CAMERAS] != [] and \
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
for variable in sensors:
|
||||
add_devices([WelcomeBinarySensor(data, camera_name, home,
|
||||
variable)])
|
||||
|
||||
|
||||
class WelcomeBinarySensor(BinarySensorDevice):
|
||||
"""Represent a single binary sensor in a Netatmo Welcome device."""
|
||||
|
||||
def __init__(self, data, camera_name, home, sensor):
|
||||
"""Setup for access to the Netatmo camera events."""
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
self._home = home
|
||||
if home:
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
self._name = camera_name
|
||||
self._sensor_name = sensor
|
||||
self._name += ' ' + sensor
|
||||
camera_id = data.welcomedata.cameraByName(camera=camera_name,
|
||||
home=home)['id']
|
||||
self._unique_id = "Welcome_binary_sensor {0} - {1}".format(self._name,
|
||||
camera_id)
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the Netatmo device and this sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID for this sensor."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def sensor_class(self):
|
||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||
return SENSOR_TYPES.get(self._sensor_name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if binary sensor is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Request an update from the Netatmo API."""
|
||||
self._data.update()
|
||||
self._data.welcomedata.updateEvent(home=self._data.home)
|
||||
|
||||
if self._sensor_name == "Someone known":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneKnownSeen(self._home,
|
||||
self._camera_name)
|
||||
elif self._sensor_name == "Someone unknown":
|
||||
self._state =\
|
||||
self._data.welcomedata.someoneUnknownSeen(self._home,
|
||||
self._camera_name)
|
||||
elif self._sensor_name == "Motion":
|
||||
self._state =\
|
||||
self._data.welcomedata.motionDetected(self._home,
|
||||
self._camera_name)
|
||||
else:
|
||||
return None
|
@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.rest/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
|
||||
import voluptuous as vol
|
||||
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
|
||||
@ -30,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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_HEADERS): {cv.string: cv.string},
|
||||
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET']),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
@ -52,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
headers = json.loads(config.get(CONF_HEADERS, '{}'))
|
||||
headers = config.get(CONF_HEADERS)
|
||||
sensor_class = config.get(CONF_SENSOR_CLASS)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
@ -70,7 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
rest.update()
|
||||
|
||||
if rest.data is None:
|
||||
_LOGGER.error('Unable to fetch REST data')
|
||||
_LOGGER.error("Unable to fetch REST data from %s", resource)
|
||||
return False
|
||||
|
||||
add_devices([RestBinarySensor(
|
||||
@ -109,8 +108,8 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
return False
|
||||
|
||||
if self._value_template is not None:
|
||||
response = self._value_template.render_with_possible_json_value(
|
||||
self.rest.data, False)
|
||||
response = self._value_template.\
|
||||
async_render_with_possible_json_value(self.rest.data, False)
|
||||
|
||||
try:
|
||||
return bool(int(response))
|
||||
|
@ -7,21 +7,20 @@ https://home-assistant.io/components/binary_sensor.tcp/
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.sensor.tcp import Sensor, CONF_VALUE_ON
|
||||
|
||||
from homeassistant.components.sensor.tcp import (
|
||||
TcpSensor, CONF_VALUE_ON, PLATFORM_SCHEMA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Create the binary sensor."""
|
||||
if not BinarySensor.validate_config(config):
|
||||
return False
|
||||
|
||||
add_entities((BinarySensor(hass, config),))
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})
|
||||
|
||||
|
||||
class BinarySensor(BinarySensorDevice, Sensor):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the TCP binary sensor."""
|
||||
add_devices([TcpBinarySensor(hass, config)])
|
||||
|
||||
|
||||
class TcpBinarySensor(BinarySensorDevice, TcpSensor):
|
||||
"""A binary sensor which is on when its state == CONF_VALUE_ON."""
|
||||
|
||||
required = (CONF_VALUE_ON,)
|
||||
|
@ -17,8 +17,8 @@ from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
|
||||
CONF_SENSOR_CLASS, CONF_SENSORS)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -35,7 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup template binary sensors."""
|
||||
sensors = []
|
||||
|
||||
@ -61,8 +62,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if not sensors:
|
||||
_LOGGER.error('No sensors added')
|
||||
return False
|
||||
add_devices(sensors)
|
||||
|
||||
hass.loop.create_task(async_add_devices(sensors))
|
||||
return True
|
||||
|
||||
|
||||
@ -74,21 +75,22 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
value_template, entity_ids):
|
||||
"""Initialize the Template binary sensor."""
|
||||
self.hass = hass
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device,
|
||||
hass=hass)
|
||||
self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device,
|
||||
hass=hass)
|
||||
self._name = friendly_name
|
||||
self._sensor_class = sensor_class
|
||||
self._template = value_template
|
||||
self._state = None
|
||||
|
||||
self.update()
|
||||
self._async_render()
|
||||
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
hass.loop.create_task(self.async_update_ha_state(True))
|
||||
|
||||
track_state_change(hass, entity_ids, template_bsensor_state_listener)
|
||||
async_track_state_change(
|
||||
hass, entity_ids, template_bsensor_state_listener)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -112,7 +114,11 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the state."""
|
||||
"""Update the state from the template."""
|
||||
self._async_render()
|
||||
|
||||
def _async_render(self):
|
||||
"""Render the state from the template."""
|
||||
try:
|
||||
self._state = self._template.async_render().lower() == 'true'
|
||||
except TemplateError as ex:
|
||||
|
@ -5,12 +5,11 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.netatmo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.components.netatmo import WelcomeData
|
||||
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@ -22,8 +21,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CONF_HOME = 'home'
|
||||
CONF_CAMERAS = 'cameras'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOME): cv.string,
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
@ -39,15 +36,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
import lnetatmo
|
||||
try:
|
||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||
for camera_name in data.get_camera_names():
|
||||
if CONF_CAMERAS in config:
|
||||
if config[CONF_CAMERAS] != [] and \
|
||||
camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
add_devices([WelcomeCamera(data, camera_name, home)])
|
||||
except lnetatmo.NoDevice:
|
||||
return None
|
||||
|
||||
for camera_name in data.get_camera_names():
|
||||
if config[CONF_CAMERAS] != []:
|
||||
if camera_name not in config[CONF_CAMERAS]:
|
||||
continue
|
||||
add_devices([WelcomeCamera(data, camera_name, home)])
|
||||
|
||||
|
||||
class WelcomeCamera(Camera):
|
||||
"""Representation of the images published from Welcome camera."""
|
||||
@ -61,6 +58,10 @@ class WelcomeCamera(Camera):
|
||||
self._name = home + ' / ' + camera_name
|
||||
else:
|
||||
self._name = camera_name
|
||||
camera_id = data.welcomedata.cameraByName(camera=camera_name,
|
||||
home=home)['id']
|
||||
self._unique_id = "Welcome_camera {0} - {1}".format(self._name,
|
||||
camera_id)
|
||||
self._vpnurl, self._localurl = self._data.welcomedata.cameraUrls(
|
||||
camera=camera_name
|
||||
)
|
||||
@ -87,31 +88,7 @@ class WelcomeCamera(Camera):
|
||||
"""Return the name of this Netatmo Welcome device."""
|
||||
return self._name
|
||||
|
||||
|
||||
class WelcomeData(object):
|
||||
"""Get the latest data from NetAtmo."""
|
||||
|
||||
def __init__(self, auth, home=None):
|
||||
"""Initialize the data object."""
|
||||
self.auth = auth
|
||||
self.welcomedata = None
|
||||
self.camera_names = []
|
||||
self.home = home
|
||||
|
||||
def get_camera_names(self):
|
||||
"""Return all module available on the API as a list."""
|
||||
self.update()
|
||||
if not self.home:
|
||||
for home in self.welcomedata.cameras:
|
||||
for camera in self.welcomedata.cameras[home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
else:
|
||||
for camera in self.welcomedata.cameras[self.home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
return self.camera_names
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Call the NetAtmo API to update the data."""
|
||||
import lnetatmo
|
||||
self.welcomedata = lnetatmo.WelcomeData(self.auth)
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID for this sensor."""
|
||||
return self._unique_id
|
||||
|
223
homeassistant/components/camera/synology.py
Normal file
223
homeassistant/components/camera/synology.py
Normal file
@ -0,0 +1,223 @@
|
||||
"""
|
||||
Support for Synology Surveillance Station Cameras.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.synology/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_URL, CONF_WHITELIST)
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
DEFAULT_NAME = 'Synology Camera'
|
||||
DEFAULT_STREAM_ID = '0'
|
||||
TIMEOUT = 5
|
||||
CONF_CAMERA_NAME = 'camera_name'
|
||||
CONF_STREAM_ID = 'stream_id'
|
||||
CONF_VALID_CERT = 'valid_cert'
|
||||
|
||||
QUERY_CGI = 'query.cgi'
|
||||
QUERY_API = 'SYNO.API.Info'
|
||||
AUTH_API = 'SYNO.API.Auth'
|
||||
CAMERA_API = 'SYNO.SurveillanceStation.Camera'
|
||||
STREAMING_API = 'SYNO.SurveillanceStation.VideoStream'
|
||||
SESSION_ID = '0'
|
||||
|
||||
WEBAPI_PATH = '/webapi/'
|
||||
AUTH_PATH = 'auth.cgi'
|
||||
CAMERA_PATH = 'camera.cgi'
|
||||
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
|
||||
|
||||
SYNO_API_URL = '{0}{1}{2}'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
||||
vol.Optional(CONF_VALID_CERT, default=True): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup a Synology IP Camera."""
|
||||
# Determine API to use for authentication
|
||||
syno_api_url = SYNO_API_URL.format(config.get(CONF_URL),
|
||||
WEBAPI_PATH,
|
||||
QUERY_CGI)
|
||||
query_payload = {'api': QUERY_API,
|
||||
'method': 'Query',
|
||||
'version': '1',
|
||||
'query': 'SYNO.'}
|
||||
query_req = requests.get(syno_api_url,
|
||||
params=query_payload,
|
||||
verify=config.get(CONF_VALID_CERT),
|
||||
timeout=TIMEOUT)
|
||||
query_resp = query_req.json()
|
||||
auth_path = query_resp['data'][AUTH_API]['path']
|
||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||
|
||||
# Authticate to NAS to get a session id
|
||||
syno_auth_url = SYNO_API_URL.format(config.get(CONF_URL),
|
||||
WEBAPI_PATH,
|
||||
auth_path)
|
||||
session_id = get_session_id(config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url,
|
||||
config.get(CONF_VALID_CERT))
|
||||
|
||||
# Use SessionID to get cameras in system
|
||||
syno_camera_url = SYNO_API_URL.format(config.get(CONF_URL),
|
||||
WEBAPI_PATH,
|
||||
camera_api)
|
||||
camera_payload = {'api': CAMERA_API,
|
||||
'method': 'List',
|
||||
'version': '1'}
|
||||
camera_req = requests.get(syno_camera_url,
|
||||
params=camera_payload,
|
||||
verify=config.get(CONF_VALID_CERT),
|
||||
timeout=TIMEOUT,
|
||||
cookies={'id': session_id})
|
||||
camera_resp = camera_req.json()
|
||||
cameras = camera_resp['data']['cameras']
|
||||
for camera in cameras:
|
||||
if not config.get(CONF_WHITELIST):
|
||||
camera_id = camera['id']
|
||||
snapshot_path = camera['snapshot_path']
|
||||
|
||||
add_devices([SynologyCamera(config,
|
||||
camera_id,
|
||||
camera['name'],
|
||||
snapshot_path,
|
||||
streaming_path,
|
||||
camera_path,
|
||||
auth_path)])
|
||||
|
||||
|
||||
def get_session_id(username, password, login_url, valid_cert):
|
||||
"""Get a session id."""
|
||||
auth_payload = {'api': AUTH_API,
|
||||
'method': 'Login',
|
||||
'version': '2',
|
||||
'account': username,
|
||||
'passwd': password,
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'}
|
||||
auth_req = requests.get(login_url,
|
||||
params=auth_payload,
|
||||
verify=valid_cert,
|
||||
timeout=TIMEOUT)
|
||||
auth_resp = auth_req.json()
|
||||
return auth_resp['data']['sid']
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class SynologyCamera(Camera):
|
||||
"""An implementation of a Synology NAS based IP camera."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, config, camera_id, camera_name,
|
||||
snapshot_path, streaming_path, camera_path, auth_path):
|
||||
"""Initialize a Synology Surveillance Station camera."""
|
||||
super().__init__()
|
||||
self._name = camera_name
|
||||
self._username = config.get(CONF_USERNAME)
|
||||
self._password = config.get(CONF_PASSWORD)
|
||||
self._synology_url = config.get(CONF_URL)
|
||||
self._api_url = config.get(CONF_URL) + 'webapi/'
|
||||
self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi'
|
||||
self._camera_name = config.get(CONF_CAMERA_NAME)
|
||||
self._stream_id = config.get(CONF_STREAM_ID)
|
||||
self._valid_cert = config.get(CONF_VALID_CERT)
|
||||
self._camera_id = camera_id
|
||||
self._snapshot_path = snapshot_path
|
||||
self._streaming_path = streaming_path
|
||||
self._camera_path = camera_path
|
||||
self._auth_path = auth_path
|
||||
|
||||
self._session_id = get_session_id(self._username,
|
||||
self._password,
|
||||
self._login_url,
|
||||
self._valid_cert)
|
||||
|
||||
def get_sid(self):
|
||||
"""Get a session id."""
|
||||
auth_payload = {'api': AUTH_API,
|
||||
'method': 'Login',
|
||||
'version': '2',
|
||||
'account': self._username,
|
||||
'passwd': self._password,
|
||||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'}
|
||||
auth_req = requests.get(self._login_url,
|
||||
params=auth_payload,
|
||||
verify=self._valid_cert,
|
||||
timeout=TIMEOUT)
|
||||
auth_resp = auth_req.json()
|
||||
self._session_id = auth_resp['data']['sid']
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
image_url = SYNO_API_URL.format(self._synology_url,
|
||||
WEBAPI_PATH,
|
||||
self._camera_path)
|
||||
image_payload = {'api': CAMERA_API,
|
||||
'method': 'GetSnapshot',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id}
|
||||
try:
|
||||
response = requests.get(image_url,
|
||||
params=image_payload,
|
||||
timeout=TIMEOUT,
|
||||
verify=self._valid_cert,
|
||||
cookies={'id': self._session_id})
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
return None
|
||||
|
||||
return response.content
|
||||
|
||||
def camera_stream(self):
|
||||
"""Return a MJPEG stream image response directly from the camera."""
|
||||
streaming_url = SYNO_API_URL.format(self._synology_url,
|
||||
WEBAPI_PATH,
|
||||
self._streaming_path)
|
||||
streaming_payload = {'api': STREAMING_API,
|
||||
'method': 'Stream',
|
||||
'version': '1',
|
||||
'cameraId': self._camera_id,
|
||||
'format': 'mjpeg'}
|
||||
response = requests.get(streaming_url,
|
||||
payload=streaming_payload,
|
||||
stream=True,
|
||||
timeout=TIMEOUT,
|
||||
cookies={'id': self._session_id})
|
||||
return response
|
||||
|
||||
def mjpeg_steam(self, response):
|
||||
"""Generate an HTTP MJPEG Stream from the Synology NAS."""
|
||||
stream = self.camera_stream()
|
||||
return response(
|
||||
stream.iter_content(chunk_size=1024),
|
||||
mimetype=stream.headers['CONTENT_TYPE_HEADER'],
|
||||
direct_passthrough=True
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._name
|
103
homeassistant/components/camera/verisure.py
Normal file
103
homeassistant/components/camera/verisure.py
Normal file
@ -0,0 +1,103 @@
|
||||
"""
|
||||
Camera that loads a picture from a local file.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.verisure/
|
||||
"""
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
from homeassistant.components.verisure import CONF_SMARTCAM
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Camera."""
|
||||
if not int(hub.config.get(CONF_SMARTCAM, 1)):
|
||||
return False
|
||||
directory_path = hass.config.config_dir
|
||||
if not os.access(directory_path, os.R_OK):
|
||||
_LOGGER.error("file path %s is not readable", directory_path)
|
||||
return False
|
||||
hub.update_smartcam()
|
||||
smartcams = []
|
||||
smartcams.extend([
|
||||
VerisureSmartcam(hass, value.deviceLabel, directory_path)
|
||||
for value in hub.smartcam_status.values()])
|
||||
add_devices(smartcams)
|
||||
|
||||
|
||||
class VerisureSmartcam(Camera):
|
||||
"""Local camera."""
|
||||
|
||||
def __init__(self, hass, device_id, directory_path):
|
||||
"""Initialize Verisure File Camera component."""
|
||||
super().__init__()
|
||||
|
||||
self._device_id = device_id
|
||||
self._directory_path = directory_path
|
||||
self._image = None
|
||||
self._image_id = None
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
self.delete_image)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return image response."""
|
||||
self.check_imagelist()
|
||||
if not self._image:
|
||||
_LOGGER.debug('No image to display')
|
||||
return
|
||||
_LOGGER.debug('Trying to open %s', self._image)
|
||||
with open(self._image, 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
def check_imagelist(self):
|
||||
"""Check the contents of the image list."""
|
||||
hub.update_smartcam_imagelist()
|
||||
if (self._device_id not in hub.smartcam_dict or
|
||||
not hub.smartcam_dict[self._device_id]):
|
||||
return
|
||||
images = hub.smartcam_dict[self._device_id]
|
||||
new_image_id = images[0]
|
||||
_LOGGER.debug('self._device_id=%s, self._images=%s, '
|
||||
'self._new_image_id=%s', self._device_id,
|
||||
images, new_image_id)
|
||||
if (new_image_id == '-1' or
|
||||
self._image_id == new_image_id):
|
||||
_LOGGER.debug('The image is the same, or loading image_id')
|
||||
return
|
||||
_LOGGER.debug('Download new image %s', new_image_id)
|
||||
hub.my_pages.smartcam.download_image(self._device_id,
|
||||
new_image_id,
|
||||
self._directory_path)
|
||||
_LOGGER.debug('Old image_id=%s', self._image_id)
|
||||
self.delete_image(self)
|
||||
|
||||
self._image_id = new_image_id
|
||||
self._image = os.path.join(self._directory_path,
|
||||
'{}{}'.format(
|
||||
self._image_id,
|
||||
'.jpg'))
|
||||
|
||||
def delete_image(self, event):
|
||||
"""Delete an old image."""
|
||||
remove_image = os.path.join(self._directory_path,
|
||||
'{}{}'.format(
|
||||
self._image_id,
|
||||
'.jpg'))
|
||||
try:
|
||||
os.remove(remove_image)
|
||||
_LOGGER.debug('Deleting old image %s', remove_image)
|
||||
except OSError as error:
|
||||
if error.errno != errno.ENOENT:
|
||||
raise
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
return hub.smartcam_status[self._device_id].location
|
@ -253,7 +253,7 @@ def setup(hass, config):
|
||||
kwargs[value] = convert_temperature(
|
||||
temp,
|
||||
hass.config.units.temperature_unit,
|
||||
climate.unit_of_measurement
|
||||
climate.temperature_unit
|
||||
)
|
||||
else:
|
||||
kwargs[value] = temp
|
||||
@ -368,7 +368,10 @@ class ClimateDevice(Entity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
return self.current_operation or STATE_UNKNOWN
|
||||
if self.current_operation:
|
||||
return self.current_operation
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
@ -398,17 +401,20 @@ class ClimateDevice(Entity):
|
||||
fan_mode = self.current_fan_mode
|
||||
if fan_mode is not None:
|
||||
data[ATTR_FAN_MODE] = fan_mode
|
||||
data[ATTR_FAN_LIST] = self.fan_list
|
||||
if self.fan_list:
|
||||
data[ATTR_FAN_LIST] = self.fan_list
|
||||
|
||||
operation_mode = self.current_operation
|
||||
if operation_mode is not None:
|
||||
data[ATTR_OPERATION_MODE] = operation_mode
|
||||
data[ATTR_OPERATION_LIST] = self.operation_list
|
||||
if self.operation_list:
|
||||
data[ATTR_OPERATION_LIST] = self.operation_list
|
||||
|
||||
swing_mode = self.current_swing_mode
|
||||
if swing_mode is not None:
|
||||
data[ATTR_SWING_MODE] = swing_mode
|
||||
data[ATTR_SWING_LIST] = self.swing_list
|
||||
if self.swing_list:
|
||||
data[ATTR_SWING_LIST] = self.swing_list
|
||||
|
||||
is_away = self.is_away_mode_on
|
||||
if is_away is not None:
|
||||
@ -422,7 +428,12 @@ class ClimateDevice(Entity):
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
"""The unit of measurement to display."""
|
||||
return self.hass.config.units.temperature_unit
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""The unit of measurement used by the platform."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
@ -534,12 +545,12 @@ class ClimateDevice(Entity):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return convert_temperature(7, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
return convert_temperature(7, TEMP_CELSIUS, self.temperature_unit)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return convert_temperature(35, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
return convert_temperature(35, TEMP_CELSIUS, self.temperature_unit)
|
||||
|
||||
@property
|
||||
def min_humidity(self):
|
||||
@ -556,10 +567,10 @@ class ClimateDevice(Entity):
|
||||
if temp is None or not isinstance(temp, Number):
|
||||
return temp
|
||||
|
||||
value = convert_temperature(temp, self.unit_of_measurement,
|
||||
self.hass.config.units.temperature_unit)
|
||||
value = convert_temperature(temp, self.temperature_unit,
|
||||
self.unit_of_measurement)
|
||||
|
||||
if self.hass.config.units.temperature_unit is TEMP_CELSIUS:
|
||||
if self.unit_of_measurement is TEMP_CELSIUS:
|
||||
decimal_count = 1
|
||||
else:
|
||||
# Users of fahrenheit generally expect integer units.
|
||||
|
@ -59,7 +59,7 @@ class DemoClimate(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.climate import (
|
||||
DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT)
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@ -105,12 +105,9 @@ class Thermostat(ClimateDevice):
|
||||
return self.thermostat['name']
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
if self.thermostat['settings']['useCelsius']:
|
||||
return TEMP_CELSIUS
|
||||
else:
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
@ -1,24 +1,38 @@
|
||||
"""
|
||||
Support for eq3 Bluetooth Smart thermostats.
|
||||
Support for eQ-3 Bluetooth Smart thermostats.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.eq3btsmart/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.const import TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE)
|
||||
from homeassistant.util.temperature import convert
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['bluepy_devices==0.2.0']
|
||||
|
||||
CONF_MAC = 'mac'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MODE = 'mode'
|
||||
ATTR_MODE_READABLE = 'mode_readable'
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_MAC): cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICES):
|
||||
vol.Schema({cv.string: DEVICE_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the eq3 BLE thermostats."""
|
||||
"""Setup the eQ-3 BLE thermostats."""
|
||||
devices = []
|
||||
|
||||
for name, device_cfg in config[CONF_DEVICES].items():
|
||||
@ -30,14 +44,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, import-error, abstract-method
|
||||
class EQ3BTSmartThermostat(ClimateDevice):
|
||||
"""Representation of a EQ3 Bluetooth Smart thermostat."""
|
||||
"""Representation of a eQ-3 Bluetooth Smart thermostat."""
|
||||
|
||||
def __init__(self, _mac, _name):
|
||||
"""Initialize the thermostat."""
|
||||
from bluepy_devices.devices import eq3btsmart
|
||||
|
||||
self._name = _name
|
||||
|
||||
self._thermostat = eq3btsmart.EQ3BTSmartThermostat(_mac)
|
||||
|
||||
@property
|
||||
@ -46,7 +59,7 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement that is used."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@ -70,8 +83,10 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
return {"mode": self._thermostat.mode,
|
||||
"mode_readable": self._thermostat.mode_readable}
|
||||
return {
|
||||
ATTR_MODE: self._thermostat.mode,
|
||||
ATTR_MODE_READABLE: self._thermostat.mode_readable,
|
||||
}
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
|
@ -100,7 +100,7 @@ class GenericThermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
|
@ -1,56 +1,54 @@
|
||||
"""
|
||||
Support for the PRT Heatmiser themostats using the V3 protocol.
|
||||
|
||||
See https://github.com/andylockran/heatmiserV3 for more info on the
|
||||
heatmiserV3 module dependency.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.heatmiser/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
import voluptuous as vol
|
||||
|
||||
CONF_IPADDRESS = 'ipaddress'
|
||||
CONF_PORT = 'port'
|
||||
CONF_TSTATS = 'tstats'
|
||||
from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_PORT, CONF_NAME, CONF_ID)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ["heatmiserV3==0.9.1"]
|
||||
REQUIREMENTS = ['heatmiserV3==0.9.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IPADDRESS = 'ipaddress'
|
||||
CONF_TSTATS = 'tstats'
|
||||
|
||||
TSTATS_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ID): cv.string,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_IPADDRESS): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
vol.Required(CONF_TSTATS, default={}):
|
||||
vol.Schema({cv.string: TSTATS_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the heatmiser thermostat."""
|
||||
from heatmiserV3 import heatmiser, connection
|
||||
|
||||
ipaddress = str(config[CONF_IPADDRESS])
|
||||
port = str(config[CONF_PORT])
|
||||
|
||||
if ipaddress is None or port is None:
|
||||
_LOGGER.error("Missing required configuration items %s or %s",
|
||||
CONF_IPADDRESS, CONF_PORT)
|
||||
return False
|
||||
ipaddress = config.get(CONF_IPADDRESS)
|
||||
port = str(config.get(CONF_PORT))
|
||||
tstats = config.get(CONF_TSTATS)
|
||||
|
||||
serport = connection.connection(ipaddress, port)
|
||||
serport.open()
|
||||
|
||||
tstats = []
|
||||
if CONF_TSTATS in config:
|
||||
tstats = config[CONF_TSTATS]
|
||||
|
||||
if tstats is None:
|
||||
_LOGGER.error("No thermostats configured.")
|
||||
return False
|
||||
|
||||
for tstat in tstats:
|
||||
for thermostat, tstat in tstats.items():
|
||||
add_devices([
|
||||
HeatmiserV3Thermostat(
|
||||
heatmiser,
|
||||
tstat.get("id"),
|
||||
tstat.get("name"),
|
||||
serport)
|
||||
heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport)
|
||||
])
|
||||
return
|
||||
|
||||
@ -69,7 +67,7 @@ class HeatmiserV3Thermostat(ClimateDevice):
|
||||
self._id = device
|
||||
self.dcb = None
|
||||
self.update()
|
||||
self._target_temperature = int(self.dcb.get("roomset"))
|
||||
self._target_temperature = int(self.dcb.get('roomset'))
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -77,7 +75,7 @@ class HeatmiserV3Thermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@ -85,9 +83,9 @@ class HeatmiserV3Thermostat(ClimateDevice):
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
if self.dcb is not None:
|
||||
low = self.dcb.get("floortemplow ")
|
||||
high = self.dcb.get("floortemphigh")
|
||||
temp = (high*256 + low)/10.0
|
||||
low = self.dcb.get('floortemplow ')
|
||||
high = self.dcb.get('floortemphigh')
|
||||
temp = (high * 256 + low) / 10.0
|
||||
self._current_temperature = temp
|
||||
else:
|
||||
self._current_temperature = None
|
||||
|
@ -41,7 +41,7 @@ class HMThermostat(homematic.HMDevice, ClimateDevice):
|
||||
"""Representation of a Homematic thermostat."""
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement that is used."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
|
@ -120,7 +120,7 @@ class RoundThermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@ -217,7 +217,7 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return (TEMP_CELSIUS if self._device.temperature_unit == 'C'
|
||||
else TEMP_FAHRENHEIT)
|
||||
|
@ -63,7 +63,7 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
return True
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
|
@ -47,7 +47,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
|
||||
return self.gateway.optimistic
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return (TEMP_CELSIUS
|
||||
if self.gateway.metric else TEMP_FAHRENHEIT)
|
||||
|
@ -40,8 +40,19 @@ class NestThermostat(ClimateDevice):
|
||||
self.structure = structure
|
||||
self.device = device
|
||||
self._fan_list = [STATE_ON, STATE_AUTO]
|
||||
self._operation_list = [STATE_HEAT, STATE_COOL, STATE_AUTO,
|
||||
STATE_OFF]
|
||||
|
||||
# Not all nest devices support cooling and heating remove unused
|
||||
self._operation_list = [STATE_OFF]
|
||||
|
||||
# Add supported nest thermostat features
|
||||
if self.device.can_heat:
|
||||
self._operation_list.append(STATE_HEAT)
|
||||
|
||||
if self.device.can_cool:
|
||||
self._operation_list.append(STATE_COOL)
|
||||
|
||||
if self.device.can_heat and self.device.can_cool:
|
||||
self._operation_list.append(STATE_AUTO)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -57,18 +68,22 @@ class NestThermostat(ClimateDevice):
|
||||
return location.capitalize() + '(' + name + ')'
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
# Move these to Thermostat Device and make them global
|
||||
return {
|
||||
"humidity": self.device.humidity,
|
||||
"target_humidity": self.device.target_humidity,
|
||||
}
|
||||
if self.device.has_humidifier or self.device.has_dehumidifier:
|
||||
# Move these to Thermostat Device and make them global
|
||||
return {
|
||||
"humidity": self.device.humidity,
|
||||
"target_humidity": self.device.target_humidity,
|
||||
}
|
||||
else:
|
||||
# No way to control humidity not show setting
|
||||
return {}
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
@ -164,7 +179,12 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return whether the fan is on."""
|
||||
return STATE_ON if self.device.fan else STATE_AUTO
|
||||
if self.device.has_fan:
|
||||
# Return whether the fan is on
|
||||
return STATE_ON if self.device.fan else STATE_AUTO
|
||||
else:
|
||||
# No Fan available so disable slider
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
|
178
homeassistant/components/climate/netatmo.py
Executable file
178
homeassistant/components/climate/netatmo.py
Executable file
@ -0,0 +1,178 @@
|
||||
"""
|
||||
Support for Netatmo Smart Thermostat.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.netatmo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.components.climate import (
|
||||
STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['netatmo']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RELAY = 'relay'
|
||||
CONF_THERMOSTAT = 'thermostat'
|
||||
|
||||
DEFAULT_AWAY_TEMPERATURE = 14
|
||||
# # The default offeset is 2 hours (when you use the thermostat itself)
|
||||
DEFAULT_TIME_OFFSET = 7200
|
||||
# # Return cached results if last scan was less then this time ago
|
||||
# # NetAtmo Data is uploaded to server every hour
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_RELAY): cv.string,
|
||||
vol.Optional(CONF_THERMOSTAT, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
||||
"""Setup the NetAtmo Thermostat."""
|
||||
netatmo = get_component('netatmo')
|
||||
device = config.get(CONF_RELAY)
|
||||
|
||||
import lnetatmo
|
||||
try:
|
||||
data = ThermostatData(netatmo.NETATMO_AUTH, device)
|
||||
for module_name in data.get_module_names():
|
||||
if CONF_THERMOSTAT in config:
|
||||
if config[CONF_THERMOSTAT] != [] and \
|
||||
module_name not in config[CONF_THERMOSTAT]:
|
||||
continue
|
||||
add_callback_devices([NetatmoThermostat(data, module_name)])
|
||||
except lnetatmo.NoDevice:
|
||||
return None
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class NetatmoThermostat(ClimateDevice):
|
||||
"""Representation a Netatmo thermostat."""
|
||||
|
||||
def __init__(self, data, module_name, away_temp=None):
|
||||
"""Initialize the sensor."""
|
||||
self._data = data
|
||||
self._state = None
|
||||
self._name = module_name
|
||||
self._target_temperature = None
|
||||
self._away = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._data.current_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return the current state of the thermostat."""
|
||||
state = self._data.thermostatdata.relay_cmd
|
||||
if state == 0:
|
||||
return STATE_IDLE
|
||||
elif state == 100:
|
||||
return STATE_HEAT
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._away
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away on."""
|
||||
mode = "away"
|
||||
temp = None
|
||||
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
|
||||
self._away = True
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away off."""
|
||||
mode = "program"
|
||||
temp = None
|
||||
self._data.thermostatdata.setthermpoint(mode, temp, endTimeOffset=None)
|
||||
self._away = False
|
||||
self.update_ha_state()
|
||||
|
||||
def set_temperature(self, endTimeOffset=DEFAULT_TIME_OFFSET, **kwargs):
|
||||
"""Set new target temperature for 2 hours."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
mode = "manual"
|
||||
self._data.thermostatdata.setthermpoint(
|
||||
mode, temperature, endTimeOffset)
|
||||
self._target_temperature = temperature
|
||||
self._away = False
|
||||
self.update_ha_state()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from NetAtmo API and updates the states."""
|
||||
self._data.update()
|
||||
self._target_temperature = self._data.thermostatdata.setpoint_temp
|
||||
self._away = self._data.setpoint_mode == 'away'
|
||||
|
||||
|
||||
class ThermostatData(object):
|
||||
"""Get the latest data from Netatmo."""
|
||||
|
||||
def __init__(self, auth, device=None):
|
||||
"""Initialize the data object."""
|
||||
self.auth = auth
|
||||
self.thermostatdata = None
|
||||
self.module_names = []
|
||||
self.device = device
|
||||
self.current_temperature = None
|
||||
self.target_temperature = None
|
||||
self.setpoint_mode = None
|
||||
# self.operation =
|
||||
|
||||
def get_module_names(self):
|
||||
"""Return all module available on the API as a list."""
|
||||
self.update()
|
||||
if not self.device:
|
||||
for device in self.thermostatdata.modules:
|
||||
for module in self.thermostatdata.modules[device].values():
|
||||
self.module_names.append(module['module_name'])
|
||||
else:
|
||||
for module in self.thermostatdata.modules[self.device].values():
|
||||
self.module_names.append(module['module_name'])
|
||||
return self.module_names
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Call the NetAtmo API to update the data."""
|
||||
import lnetatmo
|
||||
self.thermostatdata = lnetatmo.ThermostatData(self.auth)
|
||||
self.target_temperature = self.thermostatdata.setpoint_temp
|
||||
self.setpoint_mode = self.thermostatdata.setpoint_mode
|
||||
self.current_temperature = self.thermostatdata.temp
|
@ -12,7 +12,7 @@ from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['proliphix==0.3.1']
|
||||
REQUIREMENTS = ['proliphix==0.4.0']
|
||||
|
||||
ATTR_FAN = 'fan'
|
||||
|
||||
@ -69,7 +69,7 @@ class ProliphixThermostat(ClimateDevice):
|
||||
}
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
|
@ -81,7 +81,7 @@ class RadioThermostat(ClimateDevice):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
|
@ -93,7 +93,7 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
||||
self._state = self.vera_device.get_hvac_mode()
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
|
@ -209,7 +209,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
return self._swing_list
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
if self._unit == 'C':
|
||||
return TEMP_CELSIUS
|
||||
|
@ -67,31 +67,33 @@ def setup(hass, config):
|
||||
lights = sorted(hass.states.entity_ids('light'))
|
||||
switches = sorted(hass.states.entity_ids('switch'))
|
||||
media_players = sorted(hass.states.entity_ids('media_player'))
|
||||
group.Group(hass, 'living room', [
|
||||
|
||||
group.Group.create_group(hass, 'living room', [
|
||||
lights[1], switches[0], 'input_select.living_room_preset',
|
||||
'rollershutter.living_room_window', media_players[1],
|
||||
'scene.romantic_lights'])
|
||||
group.Group(hass, 'bedroom', [
|
||||
group.Group.create_group(hass, 'bedroom', [
|
||||
lights[0], switches[1], media_players[0],
|
||||
'input_slider.noise_allowance'])
|
||||
group.Group(hass, 'kitchen', [
|
||||
group.Group.create_group(hass, 'kitchen', [
|
||||
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])
|
||||
group.Group(hass, 'doors', [
|
||||
group.Group.create_group(hass, 'doors', [
|
||||
'lock.front_door', 'lock.kitchen_door',
|
||||
'garage_door.right_garage_door', 'garage_door.left_garage_door'])
|
||||
group.Group(hass, 'automations', [
|
||||
group.Group.create_group(hass, 'automations', [
|
||||
'input_select.who_cooks', 'input_boolean.notify', ])
|
||||
group.Group(hass, 'people', [
|
||||
group.Group.create_group(hass, 'people', [
|
||||
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
||||
'device_tracker.demo_paulus'])
|
||||
group.Group(hass, 'thermostats', [
|
||||
group.Group.create_group(hass, 'thermostats', [
|
||||
'thermostat.nest', 'thermostat.thermostat'])
|
||||
group.Group(hass, 'downstairs', [
|
||||
group.Group.create_group(hass, 'downstairs', [
|
||||
'group.living_room', 'group.kitchen',
|
||||
'scene.romantic_lights', 'rollershutter.kitchen_window',
|
||||
'rollershutter.living_room_window', 'group.doors', 'thermostat.nest',
|
||||
'rollershutter.living_room_window', 'group.doors',
|
||||
'thermostat.nest',
|
||||
], view=True)
|
||||
group.Group(hass, 'Upstairs', [
|
||||
group.Group.create_group(hass, 'Upstairs', [
|
||||
'thermostat.thermostat', 'group.bedroom',
|
||||
], view=True)
|
||||
|
||||
|
@ -6,6 +6,7 @@ https://home-assistant.io/components/device_tracker/
|
||||
"""
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
# pylint: disable=too-many-locals
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
@ -13,6 +14,7 @@ import threading
|
||||
from typing import Any, Sequence, Callable
|
||||
|
||||
import voluptuous as vol
|
||||
import yaml
|
||||
|
||||
from homeassistant.bootstrap import (
|
||||
prepare_setup_platform, log_exception)
|
||||
@ -25,6 +27,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util as util
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
@ -66,15 +69,11 @@ ATTR_ATTRIBUTES = 'attributes'
|
||||
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SCAN_INTERVAL): cv.positive_int, # seconds
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
_CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [
|
||||
vol.Schema({
|
||||
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
|
||||
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)
|
||||
vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
|
||||
vol.Optional(CONF_CONSIDER_HOME,
|
||||
default=timedelta(seconds=DEFAULT_CONSIDER_HOME)): vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
})
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
SERVICE_NETGEAR: 'netgear',
|
||||
@ -114,7 +113,7 @@ def setup(hass: HomeAssistantType, config: ConfigType):
|
||||
yaml_path = hass.config.path(YAML_DEVICES)
|
||||
|
||||
try:
|
||||
conf = _CONFIG_SCHEMA(config).get(DOMAIN, [])
|
||||
conf = config.get(DOMAIN, [])
|
||||
except vol.Invalid as ex:
|
||||
log_exception(ex, DOMAIN, config)
|
||||
return False
|
||||
@ -252,9 +251,18 @@ class DeviceTracker(object):
|
||||
|
||||
def setup_group(self):
|
||||
"""Initialize group for all tracked devices."""
|
||||
run_coroutine_threadsafe(
|
||||
self.async_setup_group(), self.hass.loop).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_group(self):
|
||||
"""Initialize group for all tracked devices.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
entity_ids = (dev.entity_id for dev in self.devices.values()
|
||||
if dev.track)
|
||||
self.group = group.Group(
|
||||
self.group = yield from group.Group.async_create_group(
|
||||
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
|
||||
|
||||
def update_stale(self, now: dt_util.dt.datetime):
|
||||
@ -413,7 +421,12 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
|
||||
})
|
||||
try:
|
||||
result = []
|
||||
devices = load_yaml_config_file(path)
|
||||
try:
|
||||
devices = load_yaml_config_file(path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error('Unable to load %s: %s', path, str(err))
|
||||
return []
|
||||
|
||||
for dev_id, device in devices.items():
|
||||
try:
|
||||
device = dev_schema(device)
|
||||
@ -456,14 +469,15 @@ def update_config(path: str, dev_id: str, device: Device):
|
||||
"""Add device to YAML configuration file."""
|
||||
with open(path, 'a') as out:
|
||||
out.write('\n')
|
||||
out.write('{}:\n'.format(device.dev_id))
|
||||
|
||||
for key, value in (('name', device.name), ('mac', device.mac),
|
||||
('picture', device.config_picture),
|
||||
('track', 'yes' if device.track else 'no'),
|
||||
(CONF_AWAY_HIDE,
|
||||
'yes' if device.away_hide else 'no')):
|
||||
out.write(' {}: {}\n'.format(key, '' if value is None else value))
|
||||
device = {device.dev_id: {
|
||||
'name': device.name,
|
||||
'mac': device.mac,
|
||||
'picture': device.config_picture,
|
||||
'track': device.track,
|
||||
CONF_AWAY_HIDE: device.away_hide
|
||||
}}
|
||||
yaml.dump(device, out, default_flow_style=False)
|
||||
|
||||
|
||||
def get_gravatar_for_email(email: str):
|
||||
|
82
homeassistant/components/device_tracker/bbox.py
Normal file
82
homeassistant/components/device_tracker/bbox.py
Normal file
@ -0,0 +1,82 @@
|
||||
"""
|
||||
Support for French FAI Bouygues Bbox routers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.bbox/
|
||||
"""
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pybbox==0.0.5-alpha']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Bbox scanner."""
|
||||
scanner = BboxDeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])
|
||||
|
||||
|
||||
class BboxDeviceScanner(object):
|
||||
"""This class scans for devices connected to the bbox."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.last_results = [] # type: List[Device]
|
||||
|
||||
self.success_init = self._update_info()
|
||||
_LOGGER.info('Bbox scanner initialized')
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return [device.mac for device in self.last_results]
|
||||
|
||||
def get_device_name(self, mac):
|
||||
"""Return the name of the given device or None if we don't know."""
|
||||
filter_named = [device.name for device in self.last_results if
|
||||
device.mac == mac]
|
||||
|
||||
if filter_named:
|
||||
return filter_named[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Check the bbox for devices.
|
||||
|
||||
Returns boolean if scanning successful.
|
||||
"""
|
||||
_LOGGER.info('Scanning')
|
||||
|
||||
import pybbox
|
||||
|
||||
box = pybbox.Bbox()
|
||||
result = box.get_all_connected_devices()
|
||||
|
||||
now = dt_util.now()
|
||||
last_results = []
|
||||
for device in result:
|
||||
if device['active'] != 1:
|
||||
continue
|
||||
last_results.append(
|
||||
Device(device['macaddress'], device['hostname'],
|
||||
device['ipaddress'], now))
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
_LOGGER.info('Bbox scan successful')
|
||||
return True
|
@ -79,7 +79,7 @@ class FritzBoxScanner(object):
|
||||
self._update_info()
|
||||
active_hosts = []
|
||||
for known_host in self.last_results:
|
||||
if known_host['status'] == '1':
|
||||
if known_host['status'] == '1' and known_host.get('mac'):
|
||||
active_hosts.append(known_host['mac'])
|
||||
return active_hosts
|
||||
|
||||
|
@ -11,13 +11,14 @@ import voluptuous as vol
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import CONF_DEVICES
|
||||
from homeassistant.components.mqtt import CONF_QOS
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({
|
||||
vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic},
|
||||
})
|
||||
|
||||
|
@ -30,7 +30,7 @@ CONF_EXCLUDE = 'exclude'
|
||||
REQUIREMENTS = ['python-nmap==0.6.1']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOSTS): cv.string,
|
||||
vol.Required(CONF_HOSTS): cv.ensure_list,
|
||||
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_EXCLUDE, default=[]):
|
||||
vol.All(cv.ensure_list, vol.Length(min=1))
|
||||
@ -120,7 +120,8 @@ class NmapDeviceScanner(object):
|
||||
options += ' --exclude {}'.format(','.join(exclude_hosts))
|
||||
|
||||
try:
|
||||
result = scanner.scan(hosts=self.hosts, arguments=options)
|
||||
result = scanner.scan(hosts=' '.join(self.hosts),
|
||||
arguments=options)
|
||||
except PortScannerError:
|
||||
return False
|
||||
|
||||
|
@ -23,11 +23,17 @@ _LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pysnmp==4.3.2']
|
||||
|
||||
CONF_COMMUNITY = "community"
|
||||
CONF_AUTHKEY = "authkey"
|
||||
CONF_PRIVKEY = "privkey"
|
||||
CONF_BASEOID = "baseoid"
|
||||
|
||||
DEFAULT_COMMUNITY = "public"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_COMMUNITY): cv.string,
|
||||
vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string,
|
||||
vol.Inclusive(CONF_AUTHKEY, "keys"): cv.string,
|
||||
vol.Inclusive(CONF_PRIVKEY, "keys"): cv.string,
|
||||
vol.Required(CONF_BASEOID): cv.string
|
||||
})
|
||||
|
||||
@ -43,13 +49,24 @@ def get_scanner(hass, config):
|
||||
class SnmpScanner(object):
|
||||
"""Queries any SNMP capable Access Point for connected devices."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
from pysnmp.entity.rfc3413.oneliner import cmdgen
|
||||
from pysnmp.entity import config as cfg
|
||||
self.snmp = cmdgen.CommandGenerator()
|
||||
|
||||
self.host = cmdgen.UdpTransportTarget((config[CONF_HOST], 161))
|
||||
self.community = cmdgen.CommunityData(config[CONF_COMMUNITY])
|
||||
if CONF_AUTHKEY not in config or CONF_PRIVKEY not in config:
|
||||
self.auth = cmdgen.CommunityData(config[CONF_COMMUNITY])
|
||||
else:
|
||||
self.auth = cmdgen.UsmUserData(
|
||||
config[CONF_COMMUNITY],
|
||||
config[CONF_AUTHKEY],
|
||||
config[CONF_PRIVKEY],
|
||||
authProtocol=cfg.usmHMACSHAAuthProtocol,
|
||||
privProtocol=cfg.usmAesCfb128Protocol
|
||||
)
|
||||
self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID])
|
||||
|
||||
self.lock = threading.Lock()
|
||||
@ -95,7 +112,7 @@ class SnmpScanner(object):
|
||||
devices = []
|
||||
|
||||
errindication, errstatus, errindex, restable = self.snmp.nextCmd(
|
||||
self.community, self.host, self.baseoid)
|
||||
self.auth, self.host, self.baseoid)
|
||||
|
||||
if errindication:
|
||||
_LOGGER.error("SNMPLIB error: %s", errindication)
|
||||
|
@ -13,7 +13,7 @@ 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']
|
||||
REQUIREMENTS = ['python-digitalocean==1.10.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -44,7 +44,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
|
||||
# pylint: disable=unused-argument,too-few-public-methods
|
||||
def setup(hass, config):
|
||||
"""Setup the Digital Ocean component."""
|
||||
"""Set up the Digital Ocean component."""
|
||||
conf = config[DOMAIN]
|
||||
access_token = conf.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
|
@ -14,15 +14,17 @@ import voluptuous as vol
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.discovery import load_platform, discover
|
||||
|
||||
REQUIREMENTS = ['netdisco==0.7.1']
|
||||
REQUIREMENTS = ['netdisco==0.7.2']
|
||||
|
||||
DOMAIN = 'discovery'
|
||||
|
||||
SCAN_INTERVAL = 300 # seconds
|
||||
SERVICE_NETGEAR = 'netgear_router'
|
||||
SERVICE_WEMO = 'belkin_wemo'
|
||||
SERVICE_HASS_IOS_APP = 'hass_ios'
|
||||
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||
SERVICE_NETGEAR: ('device_tracker', None),
|
||||
SERVICE_WEMO: ('wemo', None),
|
||||
'philips_hue': ('light', 'hue'),
|
||||
|
95
homeassistant/components/emoncms_history.py
Normal file
95
homeassistant/components/emoncms_history.py
Normal file
@ -0,0 +1,95 @@
|
||||
"""
|
||||
A component which allows you to send data to Emoncms.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/emoncms_history/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
import requests
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_WHITELIST,
|
||||
CONF_URL, STATE_UNKNOWN,
|
||||
STATE_UNAVAILABLE,
|
||||
CONF_SCAN_INTERVAL)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import state as state_helper
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "emoncms_history"
|
||||
CONF_INPUTNODE = "inputnode"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Required(CONF_INPUTNODE): cv.positive_int,
|
||||
vol.Required(CONF_WHITELIST): cv.entity_ids,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=30): cv.positive_int,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the emoncms_history component."""
|
||||
conf = config[DOMAIN]
|
||||
whitelist = conf.get(CONF_WHITELIST)
|
||||
|
||||
def send_data(url, apikey, node, payload):
|
||||
"""Send payload data to emoncms."""
|
||||
try:
|
||||
fullurl = "{}/input/post.json".format(url)
|
||||
req = requests.post(fullurl,
|
||||
params={"node": node},
|
||||
data={"apikey": apikey,
|
||||
"data": payload},
|
||||
allow_redirects=True,
|
||||
timeout=5)
|
||||
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error("Error saving data '%s' to '%s'",
|
||||
payload, fullurl)
|
||||
|
||||
else:
|
||||
if req.status_code != 200:
|
||||
_LOGGER.error("Error saving data '%s' to '%s'" +
|
||||
"(http status code = %d)", payload,
|
||||
fullurl, req.status_code)
|
||||
|
||||
def update_emoncms(time):
|
||||
"""Send whitelisted entities states reguarly to emoncms."""
|
||||
payload_dict = {}
|
||||
|
||||
for entity_id in whitelist:
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
if state is None or state.state in (
|
||||
STATE_UNKNOWN, "", STATE_UNAVAILABLE):
|
||||
continue
|
||||
|
||||
try:
|
||||
payload_dict[entity_id] = state_helper.state_as_number(
|
||||
state)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if len(payload_dict) > 0:
|
||||
payload = "{%s}" % ",".join("{}:{}".format(key, val)
|
||||
for key, val in
|
||||
payload_dict.items())
|
||||
|
||||
send_data(conf.get(CONF_URL), conf.get(CONF_API_KEY),
|
||||
str(conf.get(CONF_INPUTNODE)), payload)
|
||||
|
||||
track_point_in_time(hass, update_emoncms, time +
|
||||
timedelta(seconds=conf.get(
|
||||
CONF_SCAN_INTERVAL)))
|
||||
|
||||
update_emoncms(dt_util.utcnow())
|
||||
return True
|
@ -77,7 +77,7 @@ def setup(hass, yaml_config):
|
||||
ssl_certificate=None,
|
||||
ssl_key=None,
|
||||
cors_origins=[],
|
||||
approved_ips=[]
|
||||
trusted_networks=[]
|
||||
)
|
||||
|
||||
server.register_view(DescriptionXmlView(hass, config))
|
||||
|
@ -211,8 +211,14 @@ class IndexView(HomeAssistantView):
|
||||
|
||||
panel_url = PANELS[panel]['url'] if panel != 'states' else ''
|
||||
|
||||
# auto login if no password was set
|
||||
no_auth = 'false' if self.hass.config.api.api_password else 'true'
|
||||
no_auth = 'true'
|
||||
if self.hass.config.api.api_password:
|
||||
# require password if set
|
||||
no_auth = 'false'
|
||||
if self.hass.wsgi.is_trusted_ip(
|
||||
self.hass.wsgi.get_real_ip(request)):
|
||||
# bypass for trusted networks
|
||||
no_auth = 'true'
|
||||
|
||||
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
|
||||
template = self.templates.get_template('index.html')
|
||||
|
@ -1,16 +1,17 @@
|
||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "9b3e5ab4eac7e3b074e0daf3f619a638",
|
||||
"frontend.html": "5854807d361de26fe93ad474010f19d2",
|
||||
"core.js": "5ed5e063d66eb252b5b288738c9c2d16",
|
||||
"frontend.html": "0a4c2c6e86a0a78c2ff3e03842de609d",
|
||||
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002",
|
||||
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
|
||||
"panels/ha-panel-dev-service.html": "c7974458ebc33412d95497e99b785e12",
|
||||
"panels/ha-panel-dev-state.html": "4be627b74e683af14ef779d8203ec674",
|
||||
"panels/ha-panel-dev-service.html": "d33657c964041d3ebf114e90a922a15e",
|
||||
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
|
||||
"panels/ha-panel-dev-template.html": "d23943fa0370f168714da407c90091a2",
|
||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
|
||||
"panels/ha-panel-map.html": "af7d04aff7dd5479c5a0016bc8d4dd7d"
|
||||
"panels/ha-panel-map.html": "49ab2d6f180f8bdea7cffaa66b8a5d3e"
|
||||
}
|
||||
|
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 db109f5dda043182a7e9647b161851e83be9b91e
|
||||
Subproject commit f3081ed48fd11fa89586701dba3792d028473a15
|
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.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -4,9 +4,9 @@ Provides functionality to group entities.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/group/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -15,10 +15,13 @@ from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
|
||||
STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE)
|
||||
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import (
|
||||
run_callback_threadsafe, run_coroutine_threadsafe)
|
||||
|
||||
DOMAIN = 'group'
|
||||
|
||||
@ -87,7 +90,10 @@ def reload(hass):
|
||||
|
||||
|
||||
def expand_entity_ids(hass, entity_ids):
|
||||
"""Return entity_ids with group entity ids replaced by their members."""
|
||||
"""Return entity_ids with group entity ids replaced by their members.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
found_ids = []
|
||||
|
||||
for entity_id in entity_ids:
|
||||
@ -118,7 +124,10 @@ def expand_entity_ids(hass, entity_ids):
|
||||
|
||||
|
||||
def get_entity_ids(hass, entity_id, domain_filter=None):
|
||||
"""Get members of this group."""
|
||||
"""Get members of this group.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
group = hass.states.get(entity_id)
|
||||
|
||||
if not group or ATTR_ENTITY_ID not in group.attributes:
|
||||
@ -139,20 +148,19 @@ def setup(hass, config):
|
||||
"""Setup all groups found definded in the configuration."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
success = _process_config(hass, config, component)
|
||||
|
||||
if not success:
|
||||
return False
|
||||
run_coroutine_threadsafe(
|
||||
_async_process_config(hass, config, component), hass.loop).result()
|
||||
|
||||
descriptions = conf_util.load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@asyncio.coroutine
|
||||
def reload_service_handler(service_call):
|
||||
"""Remove all groups and load new ones from config."""
|
||||
conf = component.prepare_reload()
|
||||
conf = yield from component.async_prepare_reload()
|
||||
if conf is None:
|
||||
return
|
||||
_process_config(hass, conf, component)
|
||||
hass.loop.create_task(_async_process_config(hass, conf, component))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
descriptions[DOMAIN][SERVICE_RELOAD],
|
||||
@ -161,48 +169,82 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
def _process_config(hass, config, component):
|
||||
@asyncio.coroutine
|
||||
def _async_process_config(hass, config, component):
|
||||
"""Process group configuration."""
|
||||
groups = []
|
||||
for object_id, conf in config.get(DOMAIN, {}).items():
|
||||
name = conf.get(CONF_NAME, object_id)
|
||||
entity_ids = conf.get(CONF_ENTITIES) or []
|
||||
icon = conf.get(CONF_ICON)
|
||||
view = conf.get(CONF_VIEW)
|
||||
|
||||
group = Group(hass, name, entity_ids, icon=icon, view=view,
|
||||
object_id=object_id)
|
||||
component.add_entities((group,))
|
||||
# This order is important as groups get a number based on creation
|
||||
# order.
|
||||
group = yield from Group.async_create_group(
|
||||
hass, name, entity_ids, icon=icon, view=view, object_id=object_id)
|
||||
groups.append(group)
|
||||
|
||||
return True
|
||||
yield from component.async_add_entities(groups)
|
||||
|
||||
|
||||
class Group(Entity):
|
||||
"""Track a group of entity ids."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-arguments
|
||||
def __init__(self, hass, name, entity_ids=None, user_defined=True,
|
||||
icon=None, view=False, object_id=None):
|
||||
"""Initialize a group."""
|
||||
def __init__(self, hass, name, order=None, user_defined=True, icon=None,
|
||||
view=False):
|
||||
"""Initialize a group.
|
||||
|
||||
This Object has factory function for creation.
|
||||
"""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
self._order = len(hass.states.entity_ids(DOMAIN))
|
||||
self._user_defined = user_defined
|
||||
self._order = order
|
||||
self._icon = icon
|
||||
self._view = view
|
||||
self.entity_id = generate_entity_id(
|
||||
ENTITY_ID_FORMAT, object_id or name, hass=hass)
|
||||
self.tracking = []
|
||||
self.group_on = None
|
||||
self.group_off = None
|
||||
self._assumed_state = False
|
||||
self._lock = threading.Lock()
|
||||
self._unsub_state_changed = None
|
||||
self._async_unsub_state_changed = None
|
||||
|
||||
@staticmethod
|
||||
# pylint: disable=too-many-arguments
|
||||
def create_group(hass, name, entity_ids=None, user_defined=True,
|
||||
icon=None, view=False, object_id=None):
|
||||
"""Initialize a group."""
|
||||
return run_coroutine_threadsafe(
|
||||
Group.async_create_group(hass, name, entity_ids, user_defined,
|
||||
icon, view, object_id),
|
||||
hass.loop).result()
|
||||
|
||||
@staticmethod
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=too-many-arguments
|
||||
def async_create_group(hass, name, entity_ids=None, user_defined=True,
|
||||
icon=None, view=False, object_id=None):
|
||||
"""Initialize a group.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
group = Group(
|
||||
hass, name,
|
||||
order=len(hass.states.async_entity_ids(DOMAIN)),
|
||||
user_defined=user_defined, icon=icon, view=view)
|
||||
|
||||
group.entity_id = async_generate_entity_id(
|
||||
ENTITY_ID_FORMAT, object_id or name, hass=hass)
|
||||
|
||||
# run other async stuff
|
||||
if entity_ids is not None:
|
||||
self.update_tracked_entity_ids(entity_ids)
|
||||
yield from group.async_update_tracked_entity_ids(entity_ids)
|
||||
else:
|
||||
self.update_ha_state(True)
|
||||
yield from group.async_update_ha_state(True)
|
||||
|
||||
return group
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -249,40 +291,74 @@ class Group(Entity):
|
||||
|
||||
def update_tracked_entity_ids(self, entity_ids):
|
||||
"""Update the member entity IDs."""
|
||||
self.stop()
|
||||
run_coroutine_threadsafe(
|
||||
self.async_update_tracked_entity_ids(entity_ids), self.hass.loop
|
||||
).result()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_tracked_entity_ids(self, entity_ids):
|
||||
"""Update the member entity IDs.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
yield from self.async_stop()
|
||||
self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
|
||||
self.group_on, self.group_off = None, None
|
||||
|
||||
self.update_ha_state(True)
|
||||
|
||||
self.start()
|
||||
yield from self.async_update_ha_state(True)
|
||||
self.async_start()
|
||||
|
||||
def start(self):
|
||||
"""Start tracking members."""
|
||||
self._unsub_state_changed = track_state_change(
|
||||
self.hass, self.tracking, self._state_changed_listener)
|
||||
run_callback_threadsafe(self.hass.loop, self.async_start).result()
|
||||
|
||||
def async_start(self):
|
||||
"""Start tracking members.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
self._async_unsub_state_changed = async_track_state_change(
|
||||
self.hass, self.tracking, self._state_changed_listener
|
||||
)
|
||||
|
||||
def stop(self):
|
||||
"""Unregister the group from Home Assistant."""
|
||||
self.remove()
|
||||
run_coroutine_threadsafe(self.async_stop(), self.hass.loop).result()
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_stop(self):
|
||||
"""Unregister the group from Home Assistant.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
yield from self.async_remove()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Query all members and determine current group state."""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._update_group_state()
|
||||
self._async_update_group_state()
|
||||
|
||||
def remove(self):
|
||||
"""Remove group from HASS."""
|
||||
super().remove()
|
||||
@asyncio.coroutine
|
||||
def async_remove(self):
|
||||
"""Remove group from HASS.
|
||||
|
||||
if self._unsub_state_changed:
|
||||
self._unsub_state_changed()
|
||||
self._unsub_state_changed = None
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
yield from super().async_remove()
|
||||
|
||||
if self._async_unsub_state_changed:
|
||||
self._async_unsub_state_changed()
|
||||
self._async_unsub_state_changed = None
|
||||
|
||||
@callback
|
||||
def _state_changed_listener(self, entity_id, old_state, new_state):
|
||||
"""Respond to a member state changing."""
|
||||
self._update_group_state(new_state)
|
||||
self.update_ha_state()
|
||||
"""Respond to a member state changing.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
self._async_update_group_state(new_state)
|
||||
self.hass.loop.create_task(self.async_update_ha_state())
|
||||
|
||||
@property
|
||||
def _tracking_states(self):
|
||||
@ -297,62 +373,64 @@ class Group(Entity):
|
||||
|
||||
return states
|
||||
|
||||
def _update_group_state(self, tr_state=None):
|
||||
@callback
|
||||
def _async_update_group_state(self, tr_state=None):
|
||||
"""Update group state.
|
||||
|
||||
Optionally you can provide the only state changed since last update
|
||||
allowing this method to take shortcuts.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
# pylint: disable=too-many-branches
|
||||
# To store current states of group entities. Might not be needed.
|
||||
with self._lock:
|
||||
states = None
|
||||
gr_state = self._state
|
||||
gr_on = self.group_on
|
||||
gr_off = self.group_off
|
||||
states = None
|
||||
gr_state = self._state
|
||||
gr_on = self.group_on
|
||||
gr_off = self.group_off
|
||||
|
||||
# We have not determined type of group yet
|
||||
if gr_on is None:
|
||||
if tr_state is None:
|
||||
states = self._tracking_states
|
||||
# We have not determined type of group yet
|
||||
if gr_on is None:
|
||||
if tr_state is None:
|
||||
states = self._tracking_states
|
||||
|
||||
for state in states:
|
||||
gr_on, gr_off = \
|
||||
_get_group_on_off(state.state)
|
||||
if gr_on is not None:
|
||||
break
|
||||
else:
|
||||
gr_on, gr_off = _get_group_on_off(tr_state.state)
|
||||
for state in states:
|
||||
gr_on, gr_off = \
|
||||
_get_group_on_off(state.state)
|
||||
if gr_on is not None:
|
||||
break
|
||||
else:
|
||||
gr_on, gr_off = _get_group_on_off(tr_state.state)
|
||||
|
||||
if gr_on is not None:
|
||||
self.group_on, self.group_off = gr_on, gr_off
|
||||
if gr_on is not None:
|
||||
self.group_on, self.group_off = gr_on, gr_off
|
||||
|
||||
# We cannot determine state of the group
|
||||
if gr_on is None:
|
||||
return
|
||||
# We cannot determine state of the group
|
||||
if gr_on is None:
|
||||
return
|
||||
|
||||
if tr_state is None or ((gr_state == gr_on and
|
||||
tr_state.state == gr_off) or
|
||||
tr_state.state not in (gr_on, gr_off)):
|
||||
if states is None:
|
||||
states = self._tracking_states
|
||||
if tr_state is None or ((gr_state == gr_on and
|
||||
tr_state.state == gr_off) or
|
||||
tr_state.state not in (gr_on, gr_off)):
|
||||
if states is None:
|
||||
states = self._tracking_states
|
||||
|
||||
if any(state.state == gr_on for state in states):
|
||||
self._state = gr_on
|
||||
else:
|
||||
self._state = gr_off
|
||||
if any(state.state == gr_on for state in states):
|
||||
self._state = gr_on
|
||||
else:
|
||||
self._state = gr_off
|
||||
|
||||
elif tr_state.state in (gr_on, gr_off):
|
||||
self._state = tr_state.state
|
||||
elif tr_state.state in (gr_on, gr_off):
|
||||
self._state = tr_state.state
|
||||
|
||||
if tr_state is None or self._assumed_state and \
|
||||
not tr_state.attributes.get(ATTR_ASSUMED_STATE):
|
||||
if states is None:
|
||||
states = self._tracking_states
|
||||
if tr_state is None or self._assumed_state and \
|
||||
not tr_state.attributes.get(ATTR_ASSUMED_STATE):
|
||||
if states is None:
|
||||
states = self._tracking_states
|
||||
|
||||
self._assumed_state = any(
|
||||
state.attributes.get(ATTR_ASSUMED_STATE) for state
|
||||
in states)
|
||||
self._assumed_state = any(
|
||||
state.attributes.get(ATTR_ASSUMED_STATE) for state
|
||||
in states)
|
||||
|
||||
elif tr_state.attributes.get(ATTR_ASSUMED_STATE):
|
||||
self._assumed_state = True
|
||||
elif tr_state.attributes.get(ATTR_ASSUMED_STATE):
|
||||
self._assumed_state = True
|
||||
|
@ -7,16 +7,39 @@ https://home-assistant.io/components/history/
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import recorder, script
|
||||
from homeassistant.components.frontend import register_built_in_panel
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import ATTR_HIDDEN
|
||||
|
||||
DOMAIN = 'history'
|
||||
DEPENDENCIES = ['recorder', 'http']
|
||||
|
||||
SIGNIFICANT_DOMAINS = ('thermostat',)
|
||||
CONF_EXCLUDE = 'exclude'
|
||||
CONF_INCLUDE = 'include'
|
||||
CONF_ENTITIES = 'entities'
|
||||
CONF_DOMAINS = 'domains'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
CONF_EXCLUDE: vol.Schema({
|
||||
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
|
||||
vol.Optional(CONF_DOMAINS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string])
|
||||
}),
|
||||
CONF_INCLUDE: vol.Schema({
|
||||
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
|
||||
vol.Optional(CONF_DOMAINS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string])
|
||||
})
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SIGNIFICANT_DOMAINS = ('thermostat', 'climate')
|
||||
IGNORE_DOMAINS = ('zone', 'scene',)
|
||||
|
||||
|
||||
@ -32,7 +55,8 @@ def last_5_states(entity_id):
|
||||
).order_by(states.state_id.desc()).limit(5))
|
||||
|
||||
|
||||
def get_significant_states(start_time, end_time=None, entity_id=None):
|
||||
def get_significant_states(start_time, end_time=None, entity_id=None,
|
||||
filters=None):
|
||||
"""
|
||||
Return states changes during UTC period start_time - end_time.
|
||||
|
||||
@ -40,25 +64,25 @@ def get_significant_states(start_time, end_time=None, entity_id=None):
|
||||
as well as all states from certain domains (for instance
|
||||
thermostat so that we get current temperature in our graphs).
|
||||
"""
|
||||
entity_ids = (entity_id.lower(), ) if entity_id is not None else None
|
||||
states = recorder.get_model('States')
|
||||
query = recorder.query('States').filter(
|
||||
(states.domain.in_(SIGNIFICANT_DOMAINS) |
|
||||
(states.last_changed == states.last_updated)) &
|
||||
((~states.domain.in_(IGNORE_DOMAINS)) &
|
||||
(states.last_updated > start_time)))
|
||||
(states.last_updated > start_time))
|
||||
if filters:
|
||||
query = filters.apply(query, entity_ids)
|
||||
|
||||
if end_time is not None:
|
||||
query = query.filter(states.last_updated < end_time)
|
||||
|
||||
if entity_id is not None:
|
||||
query = query.filter_by(entity_id=entity_id.lower())
|
||||
|
||||
states = (
|
||||
state for state in recorder.execute(
|
||||
query.order_by(states.entity_id, states.last_updated))
|
||||
if _is_significant(state))
|
||||
if (_is_significant(state) and
|
||||
not state.attributes.get(ATTR_HIDDEN, False)))
|
||||
|
||||
return states_to_json(states, start_time, entity_id)
|
||||
return states_to_json(states, start_time, entity_id, filters)
|
||||
|
||||
|
||||
def state_changes_during_period(start_time, end_time=None, entity_id=None):
|
||||
@ -80,7 +104,7 @@ def state_changes_during_period(start_time, end_time=None, entity_id=None):
|
||||
return states_to_json(states, start_time, entity_id)
|
||||
|
||||
|
||||
def get_states(utc_point_in_time, entity_ids=None, run=None):
|
||||
def get_states(utc_point_in_time, entity_ids=None, run=None, filters=None):
|
||||
"""Return the states at a specific point in time."""
|
||||
if run is None:
|
||||
run = recorder.run_information(utc_point_in_time)
|
||||
@ -96,12 +120,11 @@ def get_states(utc_point_in_time, entity_ids=None, run=None):
|
||||
func.max(states.state_id).label('max_state_id')
|
||||
).filter(
|
||||
(states.created >= run.start) &
|
||||
(states.created < utc_point_in_time)
|
||||
)
|
||||
|
||||
if entity_ids is not None:
|
||||
most_recent_state_ids = most_recent_state_ids.filter(
|
||||
states.entity_id.in_(entity_ids))
|
||||
(states.created < utc_point_in_time) &
|
||||
(~states.domain.in_(IGNORE_DOMAINS)))
|
||||
if filters:
|
||||
most_recent_state_ids = filters.apply(most_recent_state_ids,
|
||||
entity_ids)
|
||||
|
||||
most_recent_state_ids = most_recent_state_ids.group_by(
|
||||
states.entity_id).subquery()
|
||||
@ -109,10 +132,12 @@ def get_states(utc_point_in_time, entity_ids=None, run=None):
|
||||
query = recorder.query('States').join(most_recent_state_ids, and_(
|
||||
states.state_id == most_recent_state_ids.c.max_state_id))
|
||||
|
||||
return recorder.execute(query)
|
||||
for state in recorder.execute(query):
|
||||
if not state.attributes.get(ATTR_HIDDEN, False):
|
||||
yield state
|
||||
|
||||
|
||||
def states_to_json(states, start_time, entity_id):
|
||||
def states_to_json(states, start_time, entity_id, filters=None):
|
||||
"""Convert SQL results into JSON friendly data structure.
|
||||
|
||||
This takes our state list and turns it into a JSON friendly data
|
||||
@ -127,7 +152,7 @@ def states_to_json(states, start_time, entity_id):
|
||||
entity_ids = [entity_id] if entity_id is not None else None
|
||||
|
||||
# Get the states at the start time
|
||||
for state in get_states(start_time, entity_ids):
|
||||
for state in get_states(start_time, entity_ids, filters=filters):
|
||||
state.last_changed = start_time
|
||||
state.last_updated = start_time
|
||||
result[state.entity_id].append(state)
|
||||
@ -140,16 +165,25 @@ def states_to_json(states, start_time, entity_id):
|
||||
|
||||
def get_state(utc_point_in_time, entity_id, run=None):
|
||||
"""Return a state at a specific point in time."""
|
||||
states = get_states(utc_point_in_time, (entity_id,), run)
|
||||
|
||||
states = list(get_states(utc_point_in_time, (entity_id,), run))
|
||||
return states[0] if states else None
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
"""Setup the history hooks."""
|
||||
hass.wsgi.register_view(Last5StatesView)
|
||||
hass.wsgi.register_view(HistoryPeriodView)
|
||||
filters = Filters()
|
||||
exclude = config[DOMAIN].get(CONF_EXCLUDE)
|
||||
if exclude:
|
||||
filters.excluded_entities = exclude[CONF_ENTITIES]
|
||||
filters.excluded_domains = exclude[CONF_DOMAINS]
|
||||
include = config[DOMAIN].get(CONF_INCLUDE)
|
||||
if include:
|
||||
filters.included_entities = include[CONF_ENTITIES]
|
||||
filters.included_domains = include[CONF_DOMAINS]
|
||||
|
||||
hass.wsgi.register_view(Last5StatesView(hass))
|
||||
hass.wsgi.register_view(HistoryPeriodView(hass, filters))
|
||||
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
|
||||
|
||||
return True
|
||||
@ -161,6 +195,10 @@ class Last5StatesView(HomeAssistantView):
|
||||
url = '/api/history/entity/<entity:entity_id>/recent_states'
|
||||
name = 'api:history:entity-recent-states'
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initilalize the history last 5 states view."""
|
||||
super().__init__(hass)
|
||||
|
||||
def get(self, request, entity_id):
|
||||
"""Retrieve last 5 states of entity."""
|
||||
return self.json(last_5_states(entity_id))
|
||||
@ -173,6 +211,11 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
name = 'api:history:view-period'
|
||||
extra_urls = ['/api/history/period/<datetime:datetime>']
|
||||
|
||||
def __init__(self, hass, filters):
|
||||
"""Initilalize the history period view."""
|
||||
super().__init__(hass)
|
||||
self.filters = filters
|
||||
|
||||
def get(self, request, datetime=None):
|
||||
"""Return history over a period of time."""
|
||||
one_day = timedelta(days=1)
|
||||
@ -185,8 +228,68 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
end_time = start_time + one_day
|
||||
entity_id = request.args.get('filter_entity_id')
|
||||
|
||||
return self.json(
|
||||
get_significant_states(start_time, end_time, entity_id).values())
|
||||
return self.json(get_significant_states(
|
||||
start_time, end_time, entity_id, self.filters).values())
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class Filters(object):
|
||||
"""Container for the configured include and exclude filters."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialise the include and exclude filters."""
|
||||
self.excluded_entities = []
|
||||
self.excluded_domains = []
|
||||
self.included_entities = []
|
||||
self.included_domains = []
|
||||
|
||||
def apply(self, query, entity_ids=None):
|
||||
"""Apply the include/exclude filter on domains and entities on query.
|
||||
|
||||
Following rules apply:
|
||||
* only the include section is configured - just query the specified
|
||||
entities or domains.
|
||||
* only the exclude section is configured - filter the specified
|
||||
entities and domains from all the entities in the system.
|
||||
* if include and exclude is defined - select the entities specified in
|
||||
the include and filter out the ones from the exclude list.
|
||||
"""
|
||||
states = recorder.get_model('States')
|
||||
# specific entities requested - do not in/exclude anything
|
||||
if entity_ids is not None:
|
||||
return query.filter(states.entity_id.in_(entity_ids))
|
||||
query = query.filter(~states.domain.in_(IGNORE_DOMAINS))
|
||||
|
||||
filter_query = None
|
||||
# filter if only excluded domain is configured
|
||||
if self.excluded_domains and not self.included_domains:
|
||||
filter_query = ~states.domain.in_(self.excluded_domains)
|
||||
if self.included_entities:
|
||||
filter_query &= states.entity_id.in_(self.included_entities)
|
||||
# filter if only included domain is configured
|
||||
elif not self.excluded_domains and self.included_domains:
|
||||
filter_query = states.domain.in_(self.included_domains)
|
||||
if self.included_entities:
|
||||
filter_query |= states.entity_id.in_(self.included_entities)
|
||||
# filter if included and excluded domain is configured
|
||||
elif self.excluded_domains and self.included_domains:
|
||||
filter_query = ~states.domain.in_(self.excluded_domains)
|
||||
if self.included_entities:
|
||||
filter_query &= (states.domain.in_(self.included_domains) |
|
||||
states.entity_id.in_(self.included_entities))
|
||||
else:
|
||||
filter_query &= (states.domain.in_(self.included_domains) & ~
|
||||
states.domain.in_(self.excluded_domains))
|
||||
# no domain filter just included entities
|
||||
elif not self.excluded_domains and not self.included_domains and \
|
||||
self.included_entities:
|
||||
filter_query = states.entity_id.in_(self.included_entities)
|
||||
if filter_query is not None:
|
||||
query = query.filter(filter_query)
|
||||
# finally apply excluded entities filter if configured
|
||||
if self.excluded_entities:
|
||||
query = query.filter(~states.entity_id.in_(self.excluded_entities))
|
||||
return query
|
||||
|
||||
|
||||
def _is_significant(state):
|
||||
|
@ -11,6 +11,7 @@ import mimetypes
|
||||
import threading
|
||||
import re
|
||||
import ssl
|
||||
from ipaddress import ip_address, ip_network
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -27,16 +28,16 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components import persistent_notification
|
||||
|
||||
DOMAIN = 'http'
|
||||
REQUIREMENTS = ('cherrypy==8.1.0', 'static3==0.7.0', 'Werkzeug==0.11.11')
|
||||
REQUIREMENTS = ('cherrypy==8.1.2', 'static3==0.7.0', 'Werkzeug==0.11.11')
|
||||
|
||||
CONF_API_PASSWORD = 'api_password'
|
||||
CONF_APPROVED_IPS = 'approved_ips'
|
||||
CONF_SERVER_HOST = 'server_host'
|
||||
CONF_SERVER_PORT = 'server_port'
|
||||
CONF_DEVELOPMENT = 'development'
|
||||
CONF_SSL_CERTIFICATE = 'ssl_certificate'
|
||||
CONF_SSL_KEY = 'ssl_key'
|
||||
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
||||
CONF_TRUSTED_NETWORKS = 'trusted_networks'
|
||||
|
||||
DATA_API_PASSWORD = 'api_password'
|
||||
NOTIFICATION_ID_LOGIN = 'http-login'
|
||||
@ -76,7 +77,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
|
||||
vol.Optional(CONF_SSL_KEY): cv.isfile,
|
||||
vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_APPROVED_IPS): vol.All(cv.ensure_list, [cv.string])
|
||||
vol.Optional(CONF_TRUSTED_NETWORKS):
|
||||
vol.All(cv.ensure_list, [ip_network])
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@ -113,7 +115,9 @@ def setup(hass, config):
|
||||
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
||||
ssl_key = conf.get(CONF_SSL_KEY)
|
||||
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
||||
approved_ips = conf.get(CONF_APPROVED_IPS, [])
|
||||
trusted_networks = [
|
||||
ip_network(trusted_network)
|
||||
for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])]
|
||||
|
||||
server = HomeAssistantWSGI(
|
||||
hass,
|
||||
@ -124,7 +128,7 @@ def setup(hass, config):
|
||||
ssl_certificate=ssl_certificate,
|
||||
ssl_key=ssl_key,
|
||||
cors_origins=cors_origins,
|
||||
approved_ips=approved_ips
|
||||
trusted_networks=trusted_networks
|
||||
)
|
||||
|
||||
def start_wsgi_server(event):
|
||||
@ -257,7 +261,7 @@ class HomeAssistantWSGI(object):
|
||||
|
||||
def __init__(self, hass, development, api_password, ssl_certificate,
|
||||
ssl_key, server_host, server_port, cors_origins,
|
||||
approved_ips):
|
||||
trusted_networks):
|
||||
"""Initilalize the WSGI Home Assistant server."""
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
@ -276,7 +280,7 @@ class HomeAssistantWSGI(object):
|
||||
self.server_host = server_host
|
||||
self.server_port = server_port
|
||||
self.cors_origins = cors_origins
|
||||
self.approved_ips = approved_ips
|
||||
self.trusted_networks = trusted_networks
|
||||
self.event_forwarder = None
|
||||
self.server = None
|
||||
|
||||
@ -431,6 +435,19 @@ class HomeAssistantWSGI(object):
|
||||
environ['PATH_INFO'] = '{}.{}'.format(*fingerprinted.groups())
|
||||
return app(environ, start_response)
|
||||
|
||||
@staticmethod
|
||||
def get_real_ip(request):
|
||||
"""Return the clients correct ip address, even in proxied setups."""
|
||||
if request.access_route:
|
||||
return request.access_route[-1]
|
||||
else:
|
||||
return request.remote_addr
|
||||
|
||||
def is_trusted_ip(self, remote_addr):
|
||||
"""Match an ip address against trusted CIDR networks."""
|
||||
return any(ip_address(remote_addr) in trusted_network
|
||||
for trusted_network in self.hass.wsgi.trusted_networks)
|
||||
|
||||
|
||||
class HomeAssistantView(object):
|
||||
"""Base view for all views."""
|
||||
@ -471,13 +488,15 @@ class HomeAssistantView(object):
|
||||
except AttributeError:
|
||||
raise MethodNotAllowed
|
||||
|
||||
remote_addr = HomeAssistantWSGI.get_real_ip(request)
|
||||
|
||||
# Auth code verbose on purpose
|
||||
authenticated = False
|
||||
|
||||
if self.hass.wsgi.api_password is None:
|
||||
authenticated = True
|
||||
|
||||
elif request.remote_addr in self.hass.wsgi.approved_ips:
|
||||
elif self.hass.wsgi.is_trusted_ip(remote_addr):
|
||||
authenticated = True
|
||||
|
||||
elif hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''),
|
||||
@ -491,17 +510,17 @@ class HomeAssistantView(object):
|
||||
|
||||
if self.requires_auth and not authenticated:
|
||||
_LOGGER.warning('Login attempt or request with an invalid '
|
||||
'password from %s', request.remote_addr)
|
||||
'password from %s', remote_addr)
|
||||
persistent_notification.create(
|
||||
self.hass,
|
||||
'Invalid password used from {}'.format(request.remote_addr),
|
||||
'Invalid password used from {}'.format(remote_addr),
|
||||
'Login attempt failed', NOTIFICATION_ID_LOGIN)
|
||||
raise Unauthorized()
|
||||
|
||||
request.authenticated = authenticated
|
||||
|
||||
_LOGGER.info('Serving %s to %s (auth: %s)',
|
||||
request.path, request.remote_addr, authenticated)
|
||||
request.path, remote_addr, authenticated)
|
||||
|
||||
result = handler(request, **values)
|
||||
|
||||
|
@ -33,8 +33,8 @@ TIMEOUT = 5
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_BLACKLIST, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.entity_id]),
|
||||
vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string,
|
||||
|
@ -31,6 +31,18 @@ SERVICE_SELECT_OPTION_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_OPTION): cv.string,
|
||||
})
|
||||
|
||||
SERVICE_SELECT_NEXT = 'select_next'
|
||||
|
||||
SERVICE_SELECT_NEXT_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
SERVICE_SELECT_PREVIOUS = 'select_previous'
|
||||
|
||||
SERVICE_SELECT_PREVIOUS_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
|
||||
def _cv_input_select(cfg):
|
||||
"""Config validation helper for input select (Voluptuous)."""
|
||||
@ -53,13 +65,27 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: {
|
||||
|
||||
|
||||
def select_option(hass, entity_id, option):
|
||||
"""Set input_select to False."""
|
||||
"""Set value of input_select."""
|
||||
hass.services.call(DOMAIN, SERVICE_SELECT_OPTION, {
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_OPTION: option,
|
||||
})
|
||||
|
||||
|
||||
def select_next(hass, entity_id):
|
||||
"""Set next value of input_select."""
|
||||
hass.services.call(DOMAIN, SERVICE_SELECT_NEXT, {
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
})
|
||||
|
||||
|
||||
def select_previous(hass, entity_id):
|
||||
"""Set previous value of input_select."""
|
||||
hass.services.call(DOMAIN, SERVICE_SELECT_PREVIOUS, {
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup input select."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
@ -77,7 +103,7 @@ def setup(hass, config):
|
||||
return False
|
||||
|
||||
def select_option_service(call):
|
||||
"""Handle a calls to the input select services."""
|
||||
"""Handle a calls to the input select option service."""
|
||||
target_inputs = component.extract_from_service(call)
|
||||
|
||||
for input_select in target_inputs:
|
||||
@ -87,6 +113,28 @@ def setup(hass, config):
|
||||
select_option_service,
|
||||
schema=SERVICE_SELECT_OPTION_SCHEMA)
|
||||
|
||||
def select_next_service(call):
|
||||
"""Handle a calls to the input select next service."""
|
||||
target_inputs = component.extract_from_service(call)
|
||||
|
||||
for input_select in target_inputs:
|
||||
input_select.offset_index(1)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SELECT_NEXT,
|
||||
select_next_service,
|
||||
schema=SERVICE_SELECT_NEXT_SCHEMA)
|
||||
|
||||
def select_previous_service(call):
|
||||
"""Handle a calls to the input select previous service."""
|
||||
target_inputs = component.extract_from_service(call)
|
||||
|
||||
for input_select in target_inputs:
|
||||
input_select.offset_index(-1)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SELECT_PREVIOUS,
|
||||
select_previous_service,
|
||||
schema=SERVICE_SELECT_PREVIOUS_SCHEMA)
|
||||
|
||||
component.add_entities(entities)
|
||||
|
||||
return True
|
||||
@ -139,3 +187,10 @@ class InputSelect(Entity):
|
||||
return
|
||||
self._current_option = option
|
||||
self.update_ha_state()
|
||||
|
||||
def offset_index(self, offset):
|
||||
"""Offset current index."""
|
||||
current_index = self._options.index(self._current_option)
|
||||
new_index = (current_index + offset) % len(self._options)
|
||||
self._current_option = self._options[new_index]
|
||||
self.update_ha_state()
|
||||
|
323
homeassistant/components/ios.py
Normal file
323
homeassistant/components/ios.py
Normal file
@ -0,0 +1,323 @@
|
||||
"""
|
||||
Native Home Assistant iOS app component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/ios/
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
import homeassistant.loader as loader
|
||||
|
||||
from homeassistant.helpers import discovery
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR,
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
from homeassistant.components.notify import DOMAIN as NotifyDomain
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "ios"
|
||||
|
||||
DEPENDENCIES = ["http"]
|
||||
|
||||
CONF_PUSH = "push"
|
||||
CONF_PUSH_CATEGORIES = "categories"
|
||||
CONF_PUSH_CATEGORIES_NAME = "name"
|
||||
CONF_PUSH_CATEGORIES_IDENTIFIER = "identifier"
|
||||
CONF_PUSH_CATEGORIES_ACTIONS = "actions"
|
||||
|
||||
CONF_PUSH_ACTIONS_IDENTIFIER = "identifier"
|
||||
CONF_PUSH_ACTIONS_TITLE = "title"
|
||||
CONF_PUSH_ACTIONS_ACTIVATION_MODE = "activationMode"
|
||||
CONF_PUSH_ACTIONS_AUTHENTICATION_REQUIRED = "authenticationRequired"
|
||||
CONF_PUSH_ACTIONS_DESTRUCTIVE = "destructive"
|
||||
CONF_PUSH_ACTIONS_BEHAVIOR = "behavior"
|
||||
CONF_PUSH_ACTIONS_CONTEXT = "context"
|
||||
CONF_PUSH_ACTIONS_TEXT_INPUT_BUTTON_TITLE = "textInputButtonTitle"
|
||||
CONF_PUSH_ACTIONS_TEXT_INPUT_PLACEHOLDER = "textInputPlaceholder"
|
||||
|
||||
ATTR_FOREGROUND = "foreground"
|
||||
ATTR_BACKGROUND = "background"
|
||||
|
||||
ACTIVATION_MODES = [ATTR_FOREGROUND, ATTR_BACKGROUND]
|
||||
|
||||
ATTR_DEFAULT_BEHAVIOR = "default"
|
||||
ATTR_TEXT_INPUT_BEHAVIOR = "textInput"
|
||||
|
||||
BEHAVIORS = [ATTR_DEFAULT_BEHAVIOR, ATTR_TEXT_INPUT_BEHAVIOR]
|
||||
|
||||
ATTR_DEVICE = "device"
|
||||
ATTR_PUSH_TOKEN = "pushToken"
|
||||
ATTR_APP = "app"
|
||||
ATTR_PERMISSIONS = "permissions"
|
||||
ATTR_PUSH_ID = "pushId"
|
||||
ATTR_DEVICE_ID = "deviceId"
|
||||
ATTR_PUSH_SOUNDS = "pushSounds"
|
||||
ATTR_BATTERY = "battery"
|
||||
|
||||
ATTR_DEVICE_NAME = "name"
|
||||
ATTR_DEVICE_LOCALIZED_MODEL = "localizedModel"
|
||||
ATTR_DEVICE_MODEL = "model"
|
||||
ATTR_DEVICE_PERMANENT_ID = "permanentID"
|
||||
ATTR_DEVICE_SYSTEM_VERSION = "systemVersion"
|
||||
ATTR_DEVICE_TYPE = "type"
|
||||
ATTR_DEVICE_SYSTEM_NAME = "systemName"
|
||||
|
||||
ATTR_APP_BUNDLE_IDENTIFER = "bundleIdentifer"
|
||||
ATTR_APP_BUILD_NUMBER = "buildNumber"
|
||||
ATTR_APP_VERSION_NUMBER = "versionNumber"
|
||||
|
||||
ATTR_LOCATION_PERMISSION = "location"
|
||||
ATTR_NOTIFICATIONS_PERMISSION = "notifications"
|
||||
|
||||
PERMISSIONS = [ATTR_LOCATION_PERMISSION, ATTR_NOTIFICATIONS_PERMISSION]
|
||||
|
||||
ATTR_BATTERY_STATE = "state"
|
||||
ATTR_BATTERY_LEVEL = "level"
|
||||
|
||||
ATTR_BATTERY_STATE_UNPLUGGED = "Unplugged"
|
||||
ATTR_BATTERY_STATE_CHARGING = "Charging"
|
||||
ATTR_BATTERY_STATE_FULL = "Full"
|
||||
ATTR_BATTERY_STATE_UNKNOWN = "Unknown"
|
||||
|
||||
BATTERY_STATES = [ATTR_BATTERY_STATE_UNPLUGGED, ATTR_BATTERY_STATE_CHARGING,
|
||||
ATTR_BATTERY_STATE_FULL, ATTR_BATTERY_STATE_UNKNOWN]
|
||||
|
||||
ATTR_DEVICES = "devices"
|
||||
|
||||
ACTION_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PUSH_ACTIONS_IDENTIFIER): vol.Upper,
|
||||
vol.Required(CONF_PUSH_ACTIONS_TITLE): cv.string,
|
||||
vol.Optional(CONF_PUSH_ACTIONS_ACTIVATION_MODE,
|
||||
default=ATTR_BACKGROUND): vol.In(ACTIVATION_MODES),
|
||||
vol.Optional(CONF_PUSH_ACTIONS_AUTHENTICATION_REQUIRED,
|
||||
default=False): cv.boolean,
|
||||
vol.Optional(CONF_PUSH_ACTIONS_DESTRUCTIVE,
|
||||
default=False): cv.boolean,
|
||||
vol.Optional(CONF_PUSH_ACTIONS_BEHAVIOR,
|
||||
default=ATTR_DEFAULT_BEHAVIOR): vol.In(BEHAVIORS),
|
||||
vol.Optional(CONF_PUSH_ACTIONS_TEXT_INPUT_BUTTON_TITLE): cv.string,
|
||||
vol.Optional(CONF_PUSH_ACTIONS_TEXT_INPUT_PLACEHOLDER): cv.string,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
ACTION_SCHEMA_LIST = vol.All(cv.ensure_list, [ACTION_SCHEMA])
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {
|
||||
CONF_PUSH: {
|
||||
CONF_PUSH_CATEGORIES: vol.All(cv.ensure_list, [{
|
||||
vol.Required(CONF_PUSH_CATEGORIES_NAME): cv.string,
|
||||
vol.Required(CONF_PUSH_CATEGORIES_IDENTIFIER): vol.Upper,
|
||||
vol.Required(CONF_PUSH_CATEGORIES_ACTIONS): ACTION_SCHEMA_LIST
|
||||
}])
|
||||
}
|
||||
}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
IDENTIFY_DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_DEVICE_NAME): cv.string,
|
||||
vol.Required(ATTR_DEVICE_LOCALIZED_MODEL): cv.string,
|
||||
vol.Required(ATTR_DEVICE_MODEL): cv.string,
|
||||
vol.Required(ATTR_DEVICE_PERMANENT_ID): cv.string,
|
||||
vol.Required(ATTR_DEVICE_SYSTEM_VERSION): cv.string,
|
||||
vol.Required(ATTR_DEVICE_TYPE): cv.string,
|
||||
vol.Required(ATTR_DEVICE_SYSTEM_NAME): cv.string,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
IDENTIFY_DEVICE_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_DEVICE_SCHEMA)
|
||||
|
||||
IDENTIFY_APP_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_APP_BUNDLE_IDENTIFER): cv.string,
|
||||
vol.Required(ATTR_APP_BUILD_NUMBER): cv.positive_int,
|
||||
vol.Required(ATTR_APP_VERSION_NUMBER): cv.positive_int
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
IDENTIFY_APP_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_APP_SCHEMA)
|
||||
|
||||
IDENTIFY_BATTERY_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_BATTERY_LEVEL): cv.positive_int,
|
||||
vol.Required(ATTR_BATTERY_STATE): vol.In(BATTERY_STATES)
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
IDENTIFY_BATTERY_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_BATTERY_SCHEMA)
|
||||
|
||||
IDENTIFY_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_DEVICE): IDENTIFY_DEVICE_SCHEMA_CONTAINER,
|
||||
vol.Required(ATTR_BATTERY): IDENTIFY_BATTERY_SCHEMA_CONTAINER,
|
||||
vol.Required(ATTR_PUSH_TOKEN): cv.string,
|
||||
vol.Required(ATTR_APP): IDENTIFY_APP_SCHEMA_CONTAINER,
|
||||
vol.Required(ATTR_PERMISSIONS): vol.All(cv.ensure_list,
|
||||
[vol.In(PERMISSIONS)]),
|
||||
vol.Required(ATTR_PUSH_ID): cv.string,
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
vol.Optional(ATTR_PUSH_SOUNDS): list
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
CONFIGURATION_FILE = "ios.conf"
|
||||
|
||||
CONFIG_FILE = {ATTR_DEVICES: {}}
|
||||
|
||||
CONFIG_FILE_PATH = ""
|
||||
|
||||
|
||||
def _load_config(filename):
|
||||
"""Load configuration."""
|
||||
if not os.path.isfile(filename):
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(filename, "r") as fdesc:
|
||||
inp = fdesc.read()
|
||||
|
||||
# In case empty file
|
||||
if not inp:
|
||||
return {}
|
||||
|
||||
return json.loads(inp)
|
||||
except (IOError, ValueError) as error:
|
||||
_LOGGER.error("Reading config file %s failed: %s", filename, error)
|
||||
return None
|
||||
|
||||
|
||||
def _save_config(filename, config):
|
||||
"""Save configuration."""
|
||||
try:
|
||||
with open(filename, "w") as fdesc:
|
||||
fdesc.write(json.dumps(config))
|
||||
except (IOError, TypeError) as error:
|
||||
_LOGGER.error("Saving config file failed: %s", error)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def devices_with_push():
|
||||
"""Return a dictionary of push enabled targets."""
|
||||
targets = {}
|
||||
for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
|
||||
if device.get(ATTR_PUSH_ID) is not None:
|
||||
targets[device_name] = device.get(ATTR_PUSH_ID)
|
||||
return targets
|
||||
|
||||
|
||||
def enabled_push_ids():
|
||||
"""Return a list of push enabled target push IDs."""
|
||||
push_ids = list()
|
||||
# pylint: disable=unused-variable
|
||||
for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
|
||||
if device.get(ATTR_PUSH_ID) is not None:
|
||||
push_ids.append(device.get(ATTR_PUSH_ID))
|
||||
return push_ids
|
||||
|
||||
|
||||
def devices():
|
||||
"""Return a dictionary of all identified devices."""
|
||||
return CONFIG_FILE[ATTR_DEVICES]
|
||||
|
||||
|
||||
def device_name_for_push_id(push_id):
|
||||
"""Return the device name for the push ID."""
|
||||
for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
|
||||
if device.get(ATTR_PUSH_ID) is push_id:
|
||||
return device_name
|
||||
return None
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the iOS component."""
|
||||
# pylint: disable=global-statement, import-error
|
||||
global CONFIG_FILE
|
||||
global CONFIG_FILE_PATH
|
||||
|
||||
CONFIG_FILE_PATH = hass.config.path(CONFIGURATION_FILE)
|
||||
|
||||
CONFIG_FILE = _load_config(CONFIG_FILE_PATH)
|
||||
|
||||
if CONFIG_FILE == {}:
|
||||
CONFIG_FILE[ATTR_DEVICES] = {}
|
||||
|
||||
device_tracker = loader.get_component("device_tracker")
|
||||
if device_tracker.DOMAIN not in hass.config.components:
|
||||
device_tracker.setup(hass, {})
|
||||
# Need this to enable requirements checking in the app.
|
||||
hass.config.components.append(device_tracker.DOMAIN)
|
||||
|
||||
if "notify.ios" not in hass.config.components:
|
||||
notify = loader.get_component("notify.ios")
|
||||
notify.get_service(hass, {})
|
||||
# Need this to enable requirements checking in the app.
|
||||
if NotifyDomain not in hass.config.components:
|
||||
hass.config.components.append(NotifyDomain)
|
||||
|
||||
zeroconf = loader.get_component("zeroconf")
|
||||
if zeroconf.DOMAIN not in hass.config.components:
|
||||
zeroconf.setup(hass, config)
|
||||
# Need this to enable requirements checking in the app.
|
||||
hass.config.components.append(zeroconf.DOMAIN)
|
||||
|
||||
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||
|
||||
hass.wsgi.register_view(iOSIdentifyDeviceView(hass))
|
||||
|
||||
if config.get(DOMAIN) is not None:
|
||||
app_config = config[DOMAIN]
|
||||
if app_config.get(CONF_PUSH) is not None:
|
||||
push_config = app_config[CONF_PUSH]
|
||||
hass.wsgi.register_view(iOSPushConfigView(hass, push_config))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
class iOSPushConfigView(HomeAssistantView):
|
||||
"""A view that provides the push categories configuration."""
|
||||
|
||||
url = "/api/ios/push"
|
||||
name = "api:ios:push"
|
||||
|
||||
def __init__(self, hass, push_config):
|
||||
"""Init the view."""
|
||||
super().__init__(hass)
|
||||
self.push_config = push_config
|
||||
|
||||
def get(self, request):
|
||||
"""Handle the GET request for the push configuration."""
|
||||
return self.json(self.push_config)
|
||||
|
||||
|
||||
class iOSIdentifyDeviceView(HomeAssistantView):
|
||||
"""A view that accepts device identification requests."""
|
||||
|
||||
url = "/api/ios/identify"
|
||||
name = "api:ios:identify"
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Init the view."""
|
||||
super().__init__(hass)
|
||||
|
||||
def post(self, request):
|
||||
"""Handle the POST request for device identification."""
|
||||
try:
|
||||
data = IDENTIFY_SCHEMA(request.json)
|
||||
except vol.Invalid as ex:
|
||||
return self.json_message(humanize_error(request.json, ex),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
name = data.get(ATTR_DEVICE_ID)
|
||||
|
||||
CONFIG_FILE[ATTR_DEVICES][name] = data
|
||||
|
||||
if not _save_config(CONFIG_FILE_PATH, CONFIG_FILE):
|
||||
return self.json_message("Error saving device.",
|
||||
HTTP_INTERNAL_SERVER_ERROR)
|
||||
|
||||
return self.json({"status": "registered"})
|
@ -18,7 +18,7 @@ from homeassistant.components.light import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.7.zip'
|
||||
'#flux_led==0.7']
|
||||
'#flux_led==0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -24,6 +24,26 @@ AEOTEC = 0x86
|
||||
AEOTEC_ZW098_LED_BULB = 0x62
|
||||
AEOTEC_ZW098_LED_BULB_LIGHT = (AEOTEC, AEOTEC_ZW098_LED_BULB)
|
||||
|
||||
LINEAR = 0x14f
|
||||
LINEAR_WD500Z_DIMMER = 0x3034
|
||||
LINEAR_WD500Z_DIMMER_LIGHT = (LINEAR, LINEAR_WD500Z_DIMMER)
|
||||
|
||||
GE = 0x63
|
||||
GE_12724_DIMMER = 0x3031
|
||||
GE_12724_DIMMER_LIGHT = (GE, GE_12724_DIMMER)
|
||||
|
||||
DRAGONTECH = 0x184
|
||||
DRAGONTECH_PD100_DIMMER = 0x3032
|
||||
DRAGONTECH_PD100_DIMMER_LIGHT = (DRAGONTECH, DRAGONTECH_PD100_DIMMER)
|
||||
|
||||
ACT = 0x01
|
||||
ACT_ZDP100_DIMMER = 0x3030
|
||||
ACT_ZDP100_DIMMER_LIGHT = (ACT, ACT_ZDP100_DIMMER)
|
||||
|
||||
HOMESEER = 0x0c
|
||||
HOMESEER_WD100_DIMMER = 0x3034
|
||||
HOMESEER_WD100_DIMMER_LIGHT = (HOMESEER, HOMESEER_WD100_DIMMER)
|
||||
|
||||
COLOR_CHANNEL_WARM_WHITE = 0x01
|
||||
COLOR_CHANNEL_COLD_WHITE = 0x02
|
||||
COLOR_CHANNEL_RED = 0x04
|
||||
@ -31,9 +51,15 @@ COLOR_CHANNEL_GREEN = 0x08
|
||||
COLOR_CHANNEL_BLUE = 0x10
|
||||
|
||||
WORKAROUND_ZW098 = 'zw098'
|
||||
WORKAROUND_DELAY = 'alt_delay'
|
||||
|
||||
DEVICE_MAPPINGS = {
|
||||
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098
|
||||
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098,
|
||||
LINEAR_WD500Z_DIMMER_LIGHT: WORKAROUND_DELAY,
|
||||
GE_12724_DIMMER_LIGHT: WORKAROUND_DELAY,
|
||||
DRAGONTECH_PD100_DIMMER_LIGHT: WORKAROUND_DELAY,
|
||||
ACT_ZDP100_DIMMER_LIGHT: WORKAROUND_DELAY,
|
||||
HOMESEER_WD100_DIMMER_LIGHT: WORKAROUND_DELAY,
|
||||
}
|
||||
|
||||
# Generate midpoint color temperatures for bulbs that have limited
|
||||
@ -94,6 +120,24 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._brightness = None
|
||||
self._state = None
|
||||
self._alt_delay = None
|
||||
self._zw098 = None
|
||||
|
||||
# Enable appropriate workaround flags for our device
|
||||
# Make sure that we have values for the key before converting to int
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_id, 16))
|
||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
|
||||
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
|
||||
self._zw098 = 1
|
||||
elif DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_DELAY:
|
||||
_LOGGER.debug("Dimmer delay workaround enabled for node:"
|
||||
" %s", value.parent_id)
|
||||
self._alt_delay = 1
|
||||
|
||||
self.update_properties()
|
||||
|
||||
# Used for value change event handling
|
||||
@ -125,7 +169,10 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
if self._timer is not None and self._timer.isAlive():
|
||||
self._timer.cancel()
|
||||
|
||||
self._timer = Timer(2, _refresh_value)
|
||||
if self._alt_delay:
|
||||
self._timer = Timer(5, _refresh_value)
|
||||
else:
|
||||
self._timer = Timer(2, _refresh_value)
|
||||
self._timer.start()
|
||||
|
||||
self.update_ha_state()
|
||||
@ -180,19 +227,13 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||
self._color_channels = None
|
||||
self._rgb = None
|
||||
self._ct = None
|
||||
self._zw098 = None
|
||||
|
||||
# Here we attempt to find a zwave color value with the same instance
|
||||
# id as the dimmer value. Currently zwave nodes that change colors
|
||||
# only include one dimmer and one color command, but this will
|
||||
# hopefully provide some forward compatibility for new devices that
|
||||
# have multiple color changing elements.
|
||||
# Currently zwave nodes only exist with one color element per node.
|
||||
for value_color in value.node.get_rgbbulbs().values():
|
||||
if value.instance == value_color.instance:
|
||||
self._value_color = value_color
|
||||
self._value_color = value_color
|
||||
|
||||
if self._value_color is None:
|
||||
raise ValueError("No matching color command found.")
|
||||
raise ValueError("No color command found.")
|
||||
|
||||
for value_color_channels in value.node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_COLOR,
|
||||
@ -202,17 +243,6 @@ class ZwaveColorLight(ZwaveDimmer):
|
||||
if self._value_color_channels is None:
|
||||
raise ValueError("Color Channels not found.")
|
||||
|
||||
# Make sure that we have values for the key before converting to int
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_id, 16))
|
||||
|
||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
|
||||
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
|
||||
self._zw098 = 1
|
||||
|
||||
super().__init__(value)
|
||||
|
||||
def update_properties(self):
|
||||
|
@ -14,7 +14,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||
|
||||
REQUIREMENTS = ['python-lirc==1.2.1']
|
||||
REQUIREMENTS = ['python-lirc==1.2.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -29,15 +29,22 @@ DEPENDENCIES = ['recorder', 'frontend']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_EXCLUDE = 'exclude'
|
||||
CONF_INCLUDE = 'include'
|
||||
CONF_ENTITIES = 'entities'
|
||||
CONF_DOMAINS = 'domains'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
CONF_EXCLUDE: vol.Schema({
|
||||
vol.Optional(CONF_ENTITIES, default=[]): cv.ensure_list,
|
||||
vol.Optional(CONF_DOMAINS, default=[]): cv.ensure_list
|
||||
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
|
||||
vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list,
|
||||
[cv.string])
|
||||
}),
|
||||
CONF_INCLUDE: vol.Schema({
|
||||
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
|
||||
vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list,
|
||||
[cv.string])
|
||||
})
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@ -250,7 +257,7 @@ def humanify(events):
|
||||
event.time_fired, "Home Assistant", action,
|
||||
domain=HA_DOMAIN)
|
||||
|
||||
elif event.event_type.lower() == EVENT_LOGBOOK_ENTRY:
|
||||
elif event.event_type == EVENT_LOGBOOK_ENTRY:
|
||||
domain = event.data.get(ATTR_DOMAIN)
|
||||
entity_id = event.data.get(ATTR_ENTITY_ID)
|
||||
if domain is None and entity_id is not None:
|
||||
@ -267,15 +274,24 @@ def humanify(events):
|
||||
|
||||
def _exclude_events(events, config):
|
||||
"""Get lists of excluded entities and platforms."""
|
||||
# pylint: disable=too-many-branches
|
||||
excluded_entities = []
|
||||
excluded_domains = []
|
||||
included_entities = []
|
||||
included_domains = []
|
||||
exclude = config[DOMAIN].get(CONF_EXCLUDE)
|
||||
if exclude:
|
||||
excluded_entities = exclude[CONF_ENTITIES]
|
||||
excluded_domains = exclude[CONF_DOMAINS]
|
||||
include = config[DOMAIN].get(CONF_INCLUDE)
|
||||
if include:
|
||||
included_entities = include[CONF_ENTITIES]
|
||||
included_domains = include[CONF_DOMAINS]
|
||||
|
||||
filtered_events = []
|
||||
for event in events:
|
||||
domain, entity_id = None, None
|
||||
|
||||
if event.event_type == EVENT_STATE_CHANGED:
|
||||
to_state = State.from_dict(event.data.get('new_state'))
|
||||
# Do not report on new entities
|
||||
@ -288,11 +304,38 @@ def _exclude_events(events, config):
|
||||
continue
|
||||
|
||||
domain = to_state.domain
|
||||
# check if logbook entry is excluded for this domain
|
||||
if domain in excluded_domains:
|
||||
entity_id = to_state.entity_id
|
||||
|
||||
elif event.event_type == EVENT_LOGBOOK_ENTRY:
|
||||
domain = event.data.get(ATTR_DOMAIN)
|
||||
entity_id = event.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if domain or entity_id:
|
||||
# filter if only excluded is configured for this domain
|
||||
if excluded_domains and domain in excluded_domains and \
|
||||
not included_domains:
|
||||
if (included_entities and entity_id not in included_entities) \
|
||||
or not included_entities:
|
||||
continue
|
||||
# filter if only included is configured for this domain
|
||||
elif not excluded_domains and included_domains and \
|
||||
domain not in included_domains:
|
||||
if (included_entities and entity_id not in included_entities) \
|
||||
or not included_entities:
|
||||
continue
|
||||
# filter if included and excluded is configured for this domain
|
||||
elif excluded_domains and included_domains and \
|
||||
(domain not in included_domains or
|
||||
domain in excluded_domains):
|
||||
if (included_entities and entity_id not in included_entities) \
|
||||
or not included_entities or domain in excluded_domains:
|
||||
continue
|
||||
# filter if only included is configured for this entity
|
||||
elif not excluded_domains and not included_domains and \
|
||||
included_entities and entity_id not in included_entities:
|
||||
continue
|
||||
# check if logbook entry is excluded for this entity
|
||||
if to_state.entity_id in excluded_entities:
|
||||
if entity_id in excluded_entities:
|
||||
continue
|
||||
filtered_events.append(event)
|
||||
return filtered_events
|
||||
|
@ -236,6 +236,7 @@ class BraviaTVDevice(MediaPlayerDevice):
|
||||
if power_status == 'active':
|
||||
self._state = STATE_ON
|
||||
playing_info = self._braviarc.get_playing_info()
|
||||
self._reset_playing_info()
|
||||
if playing_info is None or len(playing_info) == 0:
|
||||
self._channel_name = 'App'
|
||||
else:
|
||||
@ -255,6 +256,16 @@ class BraviaTVDevice(MediaPlayerDevice):
|
||||
_LOGGER.error(exception_instance)
|
||||
self._state = STATE_OFF
|
||||
|
||||
def _reset_playing_info(self):
|
||||
self._program_name = None
|
||||
self._channel_name = None
|
||||
self._program_media_type = None
|
||||
self._channel_number = None
|
||||
self._source = None
|
||||
self._content_uri = None
|
||||
self._duration = None
|
||||
self._start_date_time = None
|
||||
|
||||
def _refresh_volume(self):
|
||||
"""Refresh volume information."""
|
||||
volume_info = self._braviarc.get_volume_info()
|
||||
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pychromecast==0.7.4']
|
||||
REQUIREMENTS = ['pychromecast==0.7.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -68,12 +68,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
casts = []
|
||||
|
||||
# get_chromecasts() returns Chromecast objects
|
||||
# with the correct friendly name for grouped devices
|
||||
all_chromecasts = pychromecast.get_chromecasts()
|
||||
|
||||
for host in hosts:
|
||||
try:
|
||||
casts.append(CastDevice(*host))
|
||||
KNOWN_HOSTS.append(host)
|
||||
except pychromecast.ChromecastConnectionError:
|
||||
pass
|
||||
found = [device for device in all_chromecasts
|
||||
if (device.host, device.port) == host]
|
||||
if found:
|
||||
try:
|
||||
casts.append(CastDevice(found[0]))
|
||||
KNOWN_HOSTS.append(host)
|
||||
except pychromecast.ChromecastConnectionError:
|
||||
pass
|
||||
|
||||
add_devices(casts)
|
||||
|
||||
@ -83,10 +90,9 @@ class CastDevice(MediaPlayerDevice):
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
# pylint: disable=too-many-public-methods
|
||||
def __init__(self, host, port):
|
||||
def __init__(self, chromecast):
|
||||
"""Initialize the Cast device."""
|
||||
import pychromecast
|
||||
self.cast = pychromecast.Chromecast(host, port)
|
||||
self.cast = chromecast
|
||||
|
||||
self.cast.socket_client.receiver_controller.register_status_listener(
|
||||
self)
|
||||
|
@ -13,12 +13,15 @@ from homeassistant.components.media_player import (
|
||||
SUPPORT_PAUSE, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA,
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_NAME)
|
||||
CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_NAME, CONF_PORT,
|
||||
CONF_TIMEOUT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Pioneer AVR'
|
||||
DEFAULT_PORT = 23 # telnet default. Some Pioneer AVRs use 8102
|
||||
DEFAULT_TIMEOUT = None
|
||||
|
||||
SUPPORT_PIONEER = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||
@ -29,12 +32,17 @@ MAX_SOURCE_NUMBERS = 60
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.socket_timeout,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Pioneer platform."""
|
||||
pioneer = PioneerDevice(config.get(CONF_NAME), config.get(CONF_HOST))
|
||||
pioneer = PioneerDevice(config.get(CONF_NAME),
|
||||
config.get(CONF_HOST),
|
||||
config.get(CONF_PORT),
|
||||
config.get(CONF_TIMEOUT))
|
||||
|
||||
if pioneer.update():
|
||||
add_devices([pioneer])
|
||||
@ -48,10 +56,12 @@ class PioneerDevice(MediaPlayerDevice):
|
||||
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, name, host):
|
||||
def __init__(self, name, host, port, timeout):
|
||||
"""Initialize the Pioneer device."""
|
||||
self._name = name
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._timeout = timeout
|
||||
self._pwstate = 'PWR1'
|
||||
self._volume = 0
|
||||
self._muted = False
|
||||
@ -62,7 +72,11 @@ class PioneerDevice(MediaPlayerDevice):
|
||||
@classmethod
|
||||
def telnet_request(cls, telnet, command, expected_prefix):
|
||||
"""Execute `command` and return the response."""
|
||||
telnet.write(command.encode("ASCII") + b"\r")
|
||||
try:
|
||||
telnet.write(command.encode("ASCII") + b"\r")
|
||||
except telnetlib.socket.timeout:
|
||||
_LOGGER.debug("Pioneer command %s timed out", command)
|
||||
return None
|
||||
|
||||
# The receiver will randomly send state change updates, make sure
|
||||
# we get the response we are looking for
|
||||
@ -76,19 +90,32 @@ class PioneerDevice(MediaPlayerDevice):
|
||||
|
||||
def telnet_command(self, command):
|
||||
"""Establish a telnet connection and sends `command`."""
|
||||
telnet = telnetlib.Telnet(self._host)
|
||||
telnet.write(command.encode("ASCII") + b"\r")
|
||||
telnet.read_very_eager() # skip response
|
||||
telnet.close()
|
||||
try:
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self._host,
|
||||
self._port,
|
||||
self._timeout)
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.debug("Pioneer %s refused connection", self._name)
|
||||
return
|
||||
telnet.write(command.encode("ASCII") + b"\r")
|
||||
telnet.read_very_eager() # skip response
|
||||
telnet.close()
|
||||
except telnetlib.socket.timeout:
|
||||
_LOGGER.debug(
|
||||
"Pioneer %s command %s timed out", self._name, command)
|
||||
|
||||
def update(self):
|
||||
"""Get the latest details from the device."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self._host)
|
||||
telnet = telnetlib.Telnet(self._host, self._port, self._timeout)
|
||||
except ConnectionRefusedError:
|
||||
_LOGGER.debug("Pioneer %s refused connection", self._name)
|
||||
return False
|
||||
|
||||
self._pwstate = self.telnet_request(telnet, "?P", "PWR")
|
||||
pwstate = self.telnet_request(telnet, "?P", "PWR")
|
||||
if pwstate:
|
||||
self._pwstate = pwstate
|
||||
|
||||
volume_str = self.telnet_request(telnet, "?V", "VOL")
|
||||
self._volume = int(volume_str[3:]) / MAX_VOLUME if volume_str else None
|
||||
|
@ -6,23 +6,42 @@ https://home-assistant.io/components/media_player.russound_rnet/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON)
|
||||
CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/laf/russound/archive/0.1.6.zip'
|
||||
'#russound==0.1.6']
|
||||
|
||||
ZONES = 'zones'
|
||||
SOURCES = 'sources'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_SOURCES = 'sources'
|
||||
|
||||
SUPPORT_RUSSOUND = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
|
||||
SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
SOURCE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
vol.Required(CONF_ZONES): vol.Schema({cv.positive_int: ZONE_SCHEMA}),
|
||||
vol.Required(CONF_SOURCES): vol.All(cv.ensure_list, [SOURCE_SCHEMA]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@ -32,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
keypad = config.get('keypad', '70')
|
||||
|
||||
if host is None or port is None:
|
||||
_LOGGER.error('Invalid config. Expected %s and %s',
|
||||
_LOGGER.error("Invalid config. Expected %s and %s",
|
||||
CONF_HOST, CONF_PORT)
|
||||
return False
|
||||
|
||||
@ -42,13 +61,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
russ.connect(keypad)
|
||||
|
||||
sources = []
|
||||
for source in config[SOURCES]:
|
||||
for source in config[CONF_SOURCES]:
|
||||
sources.append(source['name'])
|
||||
|
||||
if russ.is_connected():
|
||||
for zone_id, extra in config[ZONES].items():
|
||||
add_devices([RussoundRNETDevice(hass, russ, sources, zone_id,
|
||||
extra)])
|
||||
for zone_id, extra in config[CONF_ZONES].items():
|
||||
add_devices([RussoundRNETDevice(
|
||||
hass, russ, sources, zone_id, extra)])
|
||||
else:
|
||||
_LOGGER.error('Not connected to %s:%s', host, port)
|
||||
|
||||
|
@ -21,7 +21,7 @@ from homeassistant.const import (
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['SoCo==0.11.1']
|
||||
REQUIREMENTS = ['SoCo==0.12']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -62,6 +62,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
if discovery_info:
|
||||
player = soco.SoCo(discovery_info)
|
||||
|
||||
# if device allready exists by config
|
||||
if player.uid in DEVICES:
|
||||
return True
|
||||
|
||||
if player.is_visible:
|
||||
device = SonosDevice(hass, player)
|
||||
add_devices([device])
|
||||
@ -212,6 +217,11 @@ class SonosDevice(MediaPlayerDevice):
|
||||
"""Update state, called by track_utc_time_change."""
|
||||
self.update_ha_state(True)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return an unique ID."""
|
||||
return self._player.uid
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
|
@ -40,6 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the squeezebox platform."""
|
||||
import socket
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
@ -50,11 +52,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
# Only add a media server once
|
||||
if host in KNOWN_DEVICES:
|
||||
# Get IP of host, to prevent duplication of same host (different DNS names)
|
||||
try:
|
||||
ipaddr = socket.gethostbyname(host)
|
||||
except (OSError) as error:
|
||||
_LOGGER.error("Could not communicate with %s:%d: %s",
|
||||
host, port, error)
|
||||
return False
|
||||
KNOWN_DEVICES.append(host)
|
||||
|
||||
# Combine it with port to allow multiple servers at the same host
|
||||
key = "{}:{}".format(ipaddr, port)
|
||||
|
||||
# Only add a media server once
|
||||
if key in KNOWN_DEVICES:
|
||||
return False
|
||||
KNOWN_DEVICES.append(key)
|
||||
|
||||
_LOGGER.debug("Creating LMS object for %s", key)
|
||||
lms = LogitechMediaServer(host, port, username, password)
|
||||
|
||||
if not lms.init_success:
|
||||
@ -97,56 +111,64 @@ class LogitechMediaServer(object):
|
||||
|
||||
def query(self, *parameters):
|
||||
"""Send request and await response from server."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host, self.port)
|
||||
if self._username and self._password:
|
||||
telnet.write('login {username} {password}\n'.format(
|
||||
username=self._username,
|
||||
password=self._password).encode('UTF-8'))
|
||||
telnet.read_until(b'\n', timeout=3)
|
||||
message = '{}\n'.format(' '.join(parameters))
|
||||
telnet.write(message.encode('UTF-8'))
|
||||
response = telnet.read_until(b'\n', timeout=3)\
|
||||
.decode('UTF-8')\
|
||||
.split(' ')[-1]\
|
||||
.strip()
|
||||
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
|
||||
response = urllib.parse.unquote(self.get(' '.join(parameters)))
|
||||
|
||||
return response.split(' ')[-1].strip()
|
||||
|
||||
def get_player_status(self, player):
|
||||
"""Get ithe status of a player."""
|
||||
"""Get the status of a player."""
|
||||
# (title) : Song title
|
||||
# Requested Information
|
||||
# a (artist): Artist name 'artist'
|
||||
# d (duration): Song duration in seconds 'duration'
|
||||
# K (artwork_url): URL to remote artwork
|
||||
tags = 'adK'
|
||||
# l (album): Album, including the server's "(N of M)"
|
||||
tags = 'adKl'
|
||||
new_status = {}
|
||||
response = self.get('{player} status - 1 tags:{tags}\n'
|
||||
.format(player=player, tags=tags))
|
||||
|
||||
if not response:
|
||||
return {}
|
||||
|
||||
response = response.split(' ')
|
||||
|
||||
for item in response:
|
||||
parts = urllib.parse.unquote(item).partition(':')
|
||||
new_status[parts[0]] = parts[2]
|
||||
return new_status
|
||||
|
||||
def get(self, command):
|
||||
"""Abstract out the telnet connection."""
|
||||
try:
|
||||
telnet = telnetlib.Telnet(self.host, self.port)
|
||||
telnet.write('{player} status - 1 tags:{tags}\n'.format(
|
||||
player=player,
|
||||
tags=tags
|
||||
).encode('UTF-8'))
|
||||
|
||||
if self._username and self._password:
|
||||
_LOGGER.debug("Logging in")
|
||||
|
||||
telnet.write('login {username} {password}\n'.format(
|
||||
username=self._username,
|
||||
password=self._password).encode('UTF-8'))
|
||||
telnet.read_until(b'\n', timeout=3)
|
||||
|
||||
_LOGGER.debug("About to send message: %s", command)
|
||||
message = '{}\n'.format(command)
|
||||
telnet.write(message.encode('UTF-8'))
|
||||
|
||||
response = telnet.read_until(b'\n', timeout=3)\
|
||||
.decode('UTF-8')\
|
||||
.split(' ')
|
||||
|
||||
telnet.write(b'exit\n')
|
||||
for item in response:
|
||||
parts = urllib.parse.unquote(item).partition(':')
|
||||
new_status[parts[0]] = parts[2]
|
||||
except (OSError, ConnectionError) as error:
|
||||
_LOGGER.debug("Response: %s", response)
|
||||
|
||||
return response
|
||||
|
||||
except (OSError, ConnectionError, EOFError) as error:
|
||||
_LOGGER.error("Could not communicate with %s:%d: %s",
|
||||
self.host,
|
||||
self.port,
|
||||
error)
|
||||
return new_status
|
||||
return None
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
@ -227,23 +249,44 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
||||
media_url = ('/music/current/cover.jpg?player={player}').format(
|
||||
player=self._id)
|
||||
|
||||
base_url = 'http://{server}:{port}/'.format(
|
||||
server=self._lms.host,
|
||||
port=self._lms.http_port)
|
||||
# pylint: disable=protected-access
|
||||
if self._lms._username:
|
||||
base_url = 'http://{username}:{password}@{server}:{port}/'.format(
|
||||
username=self._lms._username,
|
||||
password=self._lms._password,
|
||||
server=self._lms.host,
|
||||
port=self._lms.http_port)
|
||||
else:
|
||||
base_url = 'http://{server}:{port}/'.format(
|
||||
server=self._lms.host,
|
||||
port=self._lms.http_port)
|
||||
|
||||
return urllib.parse.urljoin(base_url, media_url)
|
||||
url = urllib.parse.urljoin(base_url, media_url)
|
||||
|
||||
_LOGGER.debug("Media image url: %s", url)
|
||||
return url
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
if 'artist' in self._status and 'title' in self._status:
|
||||
return '{artist} - {title}'.format(
|
||||
artist=self._status['artist'],
|
||||
title=self._status['title']
|
||||
)
|
||||
if 'title' in self._status:
|
||||
return self._status['title']
|
||||
|
||||
if 'current_title' in self._status:
|
||||
return self._status['current_title']
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media."""
|
||||
if 'artist' in self._status:
|
||||
return self._status['artist']
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Album of current playing media."""
|
||||
if 'album' in self._status:
|
||||
return self._status['album'].rstrip()
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
@ -326,7 +369,7 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
||||
"""
|
||||
Replace the current play list with the uri.
|
||||
|
||||
Telnet Command Strucutre:
|
||||
Telnet Command Structure:
|
||||
<playerid> playlist play <item> <title> <fadeInSecs>
|
||||
|
||||
The "playlist play" command puts the specified song URL,
|
||||
@ -350,7 +393,7 @@ class SqueezeBoxDevice(MediaPlayerDevice):
|
||||
"""
|
||||
Add a items to the existing playlist.
|
||||
|
||||
Telnet Command Strucutre:
|
||||
Telnet Command Structure:
|
||||
<playerid> playlist add <item>
|
||||
|
||||
The "playlist add" command adds the specified song URL, playlist or
|
||||
|
@ -10,16 +10,19 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA,
|
||||
MEDIA_TYPE_MUSIC,
|
||||
MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['rxv==0.1.11']
|
||||
REQUIREMENTS = ['rxv==0.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | \
|
||||
SUPPORT_PLAY_MEDIA
|
||||
|
||||
CONF_SOURCE_NAMES = 'source_names'
|
||||
CONF_SOURCE_IGNORE = 'source_ignore'
|
||||
@ -45,11 +48,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
source_names = config.get(CONF_SOURCE_NAMES)
|
||||
|
||||
if host is None:
|
||||
receivers = rxv.find()
|
||||
receivers = []
|
||||
for recv in rxv.find():
|
||||
receivers.extend(recv.zone_controllers())
|
||||
else:
|
||||
receivers = \
|
||||
[rxv.RXV("http://{}:80/YamahaRemoteControl/ctrl".format(host),
|
||||
name)]
|
||||
ctrl_url = "http://{}:80/YamahaRemoteControl/ctrl".format(host)
|
||||
receivers = rxv.RXV(ctrl_url, name).zone_controllers()
|
||||
|
||||
add_devices(
|
||||
YamahaDevice(name, receiver, source_ignore, source_names)
|
||||
@ -74,6 +78,7 @@ class YamahaDevice(MediaPlayerDevice):
|
||||
self._reverse_mapping = None
|
||||
self.update()
|
||||
self._name = name
|
||||
self._zone = receiver.zone
|
||||
|
||||
def update(self):
|
||||
"""Get the latest details from the device."""
|
||||
@ -104,7 +109,11 @@ class YamahaDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
name = self._name
|
||||
if self._zone != "Main_Zone":
|
||||
# Zone will be one of Main_Zone, Zone_2, Zone_3
|
||||
name += " " + self._zone.replace('_', ' ')
|
||||
return name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@ -158,3 +167,35 @@ class YamahaDevice(MediaPlayerDevice):
|
||||
def select_source(self, source):
|
||||
"""Select input source."""
|
||||
self._receiver.input = self._reverse_mapping.get(source, source)
|
||||
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play media from an ID.
|
||||
|
||||
This exposes a pass through for various input sources in the
|
||||
Yamaha to direct play certain kinds of media. media_type is
|
||||
treated as the input type that we are setting, and media id is
|
||||
specific to it.
|
||||
"""
|
||||
if media_type == "NET RADIO":
|
||||
self._receiver.net_radio(media_id)
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Return the media content type."""
|
||||
if self.source == "NET RADIO":
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Return the media title.
|
||||
|
||||
This will vary by input source, as they provide different
|
||||
information in metadata.
|
||||
|
||||
"""
|
||||
if self.source == "NET RADIO":
|
||||
info = self._receiver.play_status()
|
||||
if info.song:
|
||||
return "%s: %s" % (info.station, info.song)
|
||||
else:
|
||||
return info.station
|
||||
|
@ -18,8 +18,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import template, config_validation as cv
|
||||
from homeassistant.helpers.event import threaded_listener_factory
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_VALUE_TEMPLATE)
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -107,12 +106,11 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
MQTT_BASE_PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): DOMAIN,
|
||||
vol.Optional(CONF_SCAN_INTERVAL):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
SCHEMA_BASE = {
|
||||
vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA,
|
||||
})
|
||||
}
|
||||
|
||||
MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE)
|
||||
|
||||
# Sensor type platforms subscribe to MQTT events
|
||||
MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
@ -401,13 +399,20 @@ class MQTT(object):
|
||||
|
||||
def _mqtt_on_message(self, _mqttc, _userdata, msg):
|
||||
"""Message received callback."""
|
||||
_LOGGER.debug("received message on %s: %s",
|
||||
msg.topic, msg.payload.decode('utf-8'))
|
||||
self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
|
||||
ATTR_TOPIC: msg.topic,
|
||||
ATTR_QOS: msg.qos,
|
||||
ATTR_PAYLOAD: msg.payload.decode('utf-8'),
|
||||
})
|
||||
try:
|
||||
payload = msg.payload.decode('utf-8')
|
||||
except AttributeError:
|
||||
_LOGGER.error("Illegal utf-8 unicode payload from "
|
||||
"MQTT topic: %s, Payload: %s", msg.topic,
|
||||
msg.payload)
|
||||
else:
|
||||
_LOGGER.debug("received message on %s: %s",
|
||||
msg.topic, payload)
|
||||
self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, {
|
||||
ATTR_TOPIC: msg.topic,
|
||||
ATTR_QOS: msg.qos,
|
||||
ATTR_PAYLOAD: payload,
|
||||
})
|
||||
|
||||
def _mqtt_on_unsubscribe(self, _mqttc, _userdata, mid, granted_qos):
|
||||
"""Unsubscribe successful callback."""
|
||||
|
@ -42,7 +42,7 @@ GATEWAYS = None
|
||||
MQTT_COMPONENT = 'mqtt'
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/theolind/pymysensors/archive/'
|
||||
'8ce98b7fb56f7921a808eb66845ce8b2c455c81e.zip#pymysensors==0.7.1']
|
||||
'0b705119389be58332f17753c53167f551254b6c.zip#pymysensors==0.8']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, CONF_STRUCTURE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['python-nest==2.10.0']
|
||||
REQUIREMENTS = ['python-nest==2.11.0']
|
||||
|
||||
DOMAIN = 'nest'
|
||||
|
||||
|
@ -1,22 +1,24 @@
|
||||
"""
|
||||
Support for the Netatmo devices (Weather Station and Welcome camera).
|
||||
Support for the Netatmo devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/netatmo/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from urllib.error import HTTPError
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME)
|
||||
CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, CONF_DISCOVERY)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/jabesq/netatmo-api-python/archive/'
|
||||
'v0.5.0.zip#lnetatmo==0.5.0']
|
||||
'v0.6.0.zip#lnetatmo==0.6.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -25,6 +27,9 @@ CONF_SECRET_KEY = 'secret_key'
|
||||
DOMAIN = 'netatmo'
|
||||
|
||||
NETATMO_AUTH = None
|
||||
DEFAULT_DISCOVERY = True
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@ -32,6 +37,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_SECRET_KEY): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@ -45,12 +51,44 @@ def setup(hass, config):
|
||||
NETATMO_AUTH = lnetatmo.ClientAuth(
|
||||
config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY],
|
||||
config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD],
|
||||
'read_station read_camera access_camera')
|
||||
'read_station read_camera access_camera '
|
||||
'read_thermostat write_thermostat')
|
||||
except HTTPError:
|
||||
_LOGGER.error("Unable to connect to Netatmo API")
|
||||
return False
|
||||
|
||||
for component in 'camera', 'sensor':
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
if config[DOMAIN][CONF_DISCOVERY]:
|
||||
for component in 'camera', 'sensor', 'binary_sensor', 'climate':
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class WelcomeData(object):
|
||||
"""Get the latest data from Netatmo."""
|
||||
|
||||
def __init__(self, auth, home=None):
|
||||
"""Initialize the data object."""
|
||||
self.auth = auth
|
||||
self.welcomedata = None
|
||||
self.camera_names = []
|
||||
self.home = home
|
||||
|
||||
def get_camera_names(self):
|
||||
"""Return all module available on the API as a list."""
|
||||
self.camera_names = []
|
||||
self.update()
|
||||
if not self.home:
|
||||
for home in self.welcomedata.cameras:
|
||||
for camera in self.welcomedata.cameras[home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
else:
|
||||
for camera in self.welcomedata.cameras[self.home].values():
|
||||
self.camera_names.append(camera['name'])
|
||||
return self.camera_names
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Call the Netatmo API to update the data."""
|
||||
import lnetatmo
|
||||
self.welcomedata = lnetatmo.WelcomeData(self.auth)
|
||||
|
289
homeassistant/components/notify/apns.py
Normal file
289
homeassistant/components/notify/apns.py
Normal file
@ -0,0 +1,289 @@
|
||||
"""
|
||||
APNS Notification platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.apns/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TARGET, ATTR_DATA, BaseNotificationService)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import template as template_helper
|
||||
|
||||
DOMAIN = "apns"
|
||||
APNS_DEVICES = "apns.yaml"
|
||||
DEVICE_TRACKER_DOMAIN = "device_tracker"
|
||||
SERVICE_REGISTER = "apns_register"
|
||||
|
||||
ATTR_PUSH_ID = "push_id"
|
||||
ATTR_NAME = "name"
|
||||
|
||||
REGISTER_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_PUSH_ID): cv.string,
|
||||
vol.Optional(ATTR_NAME, default=None): cv.string,
|
||||
})
|
||||
|
||||
REQUIREMENTS = ["apns2==0.1.1"]
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Return push service."""
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
name = config.get("name")
|
||||
if name is None:
|
||||
logging.error("Name must be specified.")
|
||||
return None
|
||||
|
||||
cert_file = config.get('cert_file')
|
||||
if cert_file is None:
|
||||
logging.error("Certificate must be specified.")
|
||||
return None
|
||||
|
||||
topic = config.get('topic')
|
||||
if topic is None:
|
||||
logging.error("Topic must be specified.")
|
||||
return None
|
||||
|
||||
sandbox = bool(config.get('sandbox', False))
|
||||
|
||||
service = ApnsNotificationService(hass, name, topic, sandbox, cert_file)
|
||||
hass.services.register(DOMAIN,
|
||||
name,
|
||||
service.register,
|
||||
descriptions.get(SERVICE_REGISTER),
|
||||
schema=REGISTER_SERVICE_SCHEMA)
|
||||
return service
|
||||
|
||||
|
||||
class ApnsDevice(object):
|
||||
"""
|
||||
Apns Device class.
|
||||
|
||||
Stores information about a device that is
|
||||
registered for push notifications.
|
||||
"""
|
||||
|
||||
def __init__(self, push_id, name, tracking_device_id=None, disabled=False):
|
||||
"""Initialize Apns Device."""
|
||||
self.device_push_id = push_id
|
||||
self.device_name = name
|
||||
self.tracking_id = tracking_device_id
|
||||
self.device_disabled = disabled
|
||||
|
||||
@property
|
||||
def push_id(self):
|
||||
"""The apns id for the device."""
|
||||
return self.device_push_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The friendly name for the device."""
|
||||
return self.device_name
|
||||
|
||||
@property
|
||||
def tracking_device_id(self):
|
||||
"""
|
||||
Device Id.
|
||||
|
||||
The id of a device that is tracked by the device
|
||||
tracking component.
|
||||
"""
|
||||
return self.tracking_id
|
||||
|
||||
@property
|
||||
def full_tracking_device_id(self):
|
||||
"""
|
||||
Fully qualified device id.
|
||||
|
||||
The full id of a device that is tracked by the device
|
||||
tracking component.
|
||||
"""
|
||||
return DEVICE_TRACKER_DOMAIN + '.' + self.tracking_id
|
||||
|
||||
@property
|
||||
def disabled(self):
|
||||
"""Should receive notifications."""
|
||||
return self.device_disabled
|
||||
|
||||
def disable(self):
|
||||
"""Disable the device from recieving notifications."""
|
||||
self.device_disabled = True
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Return the comparision."""
|
||||
if isinstance(other, self.__class__):
|
||||
return self.push_id == other.push_id and self.name == other.name
|
||||
return NotImplemented
|
||||
|
||||
def __ne__(self, other):
|
||||
"""Return the comparision."""
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
class ApnsNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for the APNS service."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, hass, app_name, topic, sandbox, cert_file):
|
||||
"""Initialize APNS application."""
|
||||
self.hass = hass
|
||||
self.app_name = app_name
|
||||
self.sandbox = sandbox
|
||||
self.certificate = cert_file
|
||||
self.yaml_path = hass.config.path(app_name + '_' + APNS_DEVICES)
|
||||
self.devices = {}
|
||||
self.device_states = {}
|
||||
self.topic = topic
|
||||
if os.path.isfile(self.yaml_path):
|
||||
self.devices = {
|
||||
str(key): ApnsDevice(
|
||||
str(key),
|
||||
value.get('name'),
|
||||
value.get('tracking_device_id'),
|
||||
value.get('disabled', False)
|
||||
)
|
||||
for (key, value) in
|
||||
load_yaml_config_file(self.yaml_path).items()
|
||||
}
|
||||
|
||||
tracking_ids = [
|
||||
device.full_tracking_device_id
|
||||
for (key, device) in self.devices.items()
|
||||
if device.tracking_device_id is not None
|
||||
]
|
||||
track_state_change(
|
||||
hass,
|
||||
tracking_ids,
|
||||
self.device_state_changed_listener)
|
||||
|
||||
def device_state_changed_listener(self, entity_id, from_s, to_s):
|
||||
"""
|
||||
Listener for sate change.
|
||||
|
||||
Track device state change if a device
|
||||
has a tracking id specified.
|
||||
"""
|
||||
self.device_states[entity_id] = str(to_s.state)
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def write_device(out, device):
|
||||
"""Write a single device to file."""
|
||||
attributes = []
|
||||
if device.name is not None:
|
||||
attributes.append(
|
||||
'name: {}'.format(device.name))
|
||||
if device.tracking_device_id is not None:
|
||||
attributes.append(
|
||||
'tracking_device_id: {}'.format(device.tracking_device_id))
|
||||
if device.disabled:
|
||||
attributes.append('disabled: True')
|
||||
|
||||
out.write(device.push_id)
|
||||
out.write(": {")
|
||||
if len(attributes) > 0:
|
||||
separator = ", "
|
||||
out.write(separator.join(attributes))
|
||||
|
||||
out.write("}\n")
|
||||
|
||||
def write_devices(self):
|
||||
"""Write all known devices to file."""
|
||||
with open(self.yaml_path, 'w+') as out:
|
||||
for _, device in self.devices.items():
|
||||
ApnsNotificationService.write_device(out, device)
|
||||
|
||||
def register(self, call):
|
||||
"""Register a device to receive push messages."""
|
||||
push_id = call.data.get(ATTR_PUSH_ID)
|
||||
if push_id is None:
|
||||
return False
|
||||
|
||||
device_name = call.data.get(ATTR_NAME)
|
||||
current_device = self.devices.get(push_id)
|
||||
current_tracking_id = None if current_device is None \
|
||||
else current_device.tracking_device_id
|
||||
|
||||
device = ApnsDevice(
|
||||
push_id,
|
||||
device_name,
|
||||
current_tracking_id)
|
||||
|
||||
if current_device is None:
|
||||
self.devices[push_id] = device
|
||||
with open(self.yaml_path, 'a') as out:
|
||||
self.write_device(out, device)
|
||||
return True
|
||||
|
||||
if device != current_device:
|
||||
self.devices[push_id] = device
|
||||
self.write_devices()
|
||||
|
||||
return True
|
||||
|
||||
def send_message(self, message=None, **kwargs):
|
||||
"""Send push message to registered devices."""
|
||||
from apns2.client import APNsClient
|
||||
from apns2.payload import Payload
|
||||
from apns2.errors import Unregistered
|
||||
|
||||
apns = APNsClient(
|
||||
self.certificate,
|
||||
use_sandbox=self.sandbox,
|
||||
use_alternative_port=False)
|
||||
|
||||
device_state = kwargs.get(ATTR_TARGET)
|
||||
message_data = kwargs.get(ATTR_DATA)
|
||||
|
||||
if message_data is None:
|
||||
message_data = {}
|
||||
|
||||
if isinstance(message, str):
|
||||
rendered_message = message
|
||||
elif isinstance(message, template_helper.Template):
|
||||
rendered_message = message.render()
|
||||
else:
|
||||
rendered_message = ""
|
||||
|
||||
payload = Payload(
|
||||
alert=rendered_message,
|
||||
badge=message_data.get("badge"),
|
||||
sound=message_data.get("sound"),
|
||||
category=message_data.get("category"),
|
||||
custom=message_data.get("custom", {}),
|
||||
content_available=message_data.get("content_available", False))
|
||||
|
||||
device_update = False
|
||||
|
||||
for push_id, device in self.devices.items():
|
||||
if not device.disabled:
|
||||
state = None
|
||||
if device.tracking_device_id is not None:
|
||||
state = self.device_states.get(
|
||||
device.full_tracking_device_id)
|
||||
|
||||
if device_state is None or state == str(device_state):
|
||||
try:
|
||||
apns.send_notification(
|
||||
push_id,
|
||||
payload,
|
||||
topic=self.topic)
|
||||
except Unregistered:
|
||||
logging.error(
|
||||
"Device %s has unregistered.",
|
||||
push_id)
|
||||
device_update = True
|
||||
device.disable()
|
||||
|
||||
if device_update:
|
||||
self.write_devices()
|
||||
|
||||
return True
|
87
homeassistant/components/notify/ios.py
Normal file
87
homeassistant/components/notify/ios.py
Normal file
@ -0,0 +1,87 @@
|
||||
"""
|
||||
iOS push notification platform for notify component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.ios/
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
import requests
|
||||
|
||||
from homeassistant.components import ios
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_MESSAGE,
|
||||
ATTR_DATA, BaseNotificationService)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PUSH_URL = "https://ios-push.home-assistant.io/push"
|
||||
|
||||
DEPENDENCIES = ["ios"]
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Get the iOS notification service."""
|
||||
if "notify.ios" not in hass.config.components:
|
||||
# Need this to enable requirements checking in the app.
|
||||
hass.config.components.append("notify.ios")
|
||||
|
||||
return iOSNotificationService()
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods, too-many-arguments, invalid-name
|
||||
class iOSNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for iOS."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the service."""
|
||||
|
||||
@property
|
||||
def targets(self):
|
||||
"""Return a dictionary of registered targets."""
|
||||
return ios.devices_with_push()
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to the Lambda APNS gateway."""
|
||||
data = {ATTR_MESSAGE: message}
|
||||
|
||||
if kwargs.get(ATTR_TITLE) is not None:
|
||||
# Remove default title from notifications.
|
||||
if kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT:
|
||||
data[ATTR_TITLE] = kwargs.get(ATTR_TITLE)
|
||||
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
|
||||
if not targets:
|
||||
targets = ios.enabled_push_ids()
|
||||
|
||||
if kwargs.get(ATTR_DATA) is not None:
|
||||
data[ATTR_DATA] = kwargs.get(ATTR_DATA)
|
||||
|
||||
for target in targets:
|
||||
data[ATTR_TARGET] = target
|
||||
|
||||
req = requests.post(PUSH_URL, json=data, timeout=10)
|
||||
|
||||
if req.status_code is not 201:
|
||||
message = req.json()["message"]
|
||||
if req.status_code is 429:
|
||||
_LOGGER.warning(message)
|
||||
elif req.status_code is 400 or 500:
|
||||
_LOGGER.error(message)
|
||||
|
||||
if req.status_code in (201, 429):
|
||||
rate_limits = req.json()["rateLimits"]
|
||||
resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"])
|
||||
resetsAtTime = resetsAt - datetime.now(timezone.utc)
|
||||
rate_limit_msg = ("iOS push notification rate limits for %s: "
|
||||
"%d sent, %d allowed, %d errors, "
|
||||
"resets in %s")
|
||||
_LOGGER.info(rate_limit_msg,
|
||||
ios.device_name_for_push_id(target),
|
||||
rate_limits["successful"],
|
||||
rate_limits["maximum"], rate_limits["errors"],
|
||||
str(resetsAtTime).split(".")[0])
|
169
homeassistant/components/notify/matrix.py
Normal file
169
homeassistant/components/notify/matrix.py
Normal file
@ -0,0 +1,169 @@
|
||||
"""
|
||||
Matrix notification service.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.matrix/
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL
|
||||
|
||||
REQUIREMENTS = ['matrix-client==0.0.5']
|
||||
|
||||
SESSION_FILE = 'matrix.conf'
|
||||
AUTH_TOKENS = dict()
|
||||
|
||||
CONF_HOMESERVER = 'homeserver'
|
||||
CONF_DEFAULT_ROOM = 'default_room'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOMESERVER): cv.url,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_DEFAULT_ROOM): cv.string,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Get the Matrix notification service."""
|
||||
if not AUTH_TOKENS:
|
||||
load_token(hass.config.path(SESSION_FILE))
|
||||
|
||||
return MatrixNotificationService(
|
||||
config.get(CONF_HOMESERVER),
|
||||
config.get(CONF_DEFAULT_ROOM),
|
||||
config.get(CONF_VERIFY_SSL),
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD)
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class MatrixNotificationService(BaseNotificationService):
|
||||
"""Wrapper for the MatrixNotificationClient."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, homeserver, default_room, verify_ssl,
|
||||
username, password):
|
||||
"""Buffer configuration data for send_message."""
|
||||
self.homeserver = homeserver
|
||||
self.default_room = default_room
|
||||
self.verify_tls = verify_ssl
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def send_message(self, message, **kwargs):
|
||||
"""Wrapper function pass default parameters to actual send_message."""
|
||||
send_message(
|
||||
message,
|
||||
self.homeserver,
|
||||
kwargs.get(ATTR_TARGET) or [self.default_room],
|
||||
self.verify_tls,
|
||||
self.username,
|
||||
self.password
|
||||
)
|
||||
|
||||
|
||||
def load_token(session_file):
|
||||
"""Load authentication tokens from persistent storage, if exists."""
|
||||
if not os.path.exists(session_file):
|
||||
return
|
||||
|
||||
with open(session_file) as handle:
|
||||
data = json.load(handle)
|
||||
|
||||
for mx_id, token in data.items():
|
||||
AUTH_TOKENS[mx_id] = token
|
||||
|
||||
|
||||
def store_token(mx_id, token):
|
||||
"""Store authentication token to session and persistent storage."""
|
||||
AUTH_TOKENS[mx_id] = token
|
||||
|
||||
with open(SESSION_FILE, 'w') as handle:
|
||||
handle.write(json.dumps(AUTH_TOKENS))
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals, too-many-arguments
|
||||
def send_message(message, homeserver, target_rooms, verify_tls,
|
||||
username, password):
|
||||
"""Do everything thats necessary to send a message to a Matrix room."""
|
||||
from matrix_client.client import MatrixClient, MatrixRequestError
|
||||
|
||||
def login_by_token():
|
||||
"""Login using authentication token."""
|
||||
try:
|
||||
return MatrixClient(
|
||||
base_url=homeserver,
|
||||
token=AUTH_TOKENS[mx_id],
|
||||
user_id=username,
|
||||
valid_cert_check=verify_tls
|
||||
)
|
||||
except MatrixRequestError as ex:
|
||||
_LOGGER.info(
|
||||
'login_by_token: (%d) %s', ex.code, ex.content
|
||||
)
|
||||
|
||||
def login_by_password():
|
||||
"""Login using password authentication."""
|
||||
try:
|
||||
_client = MatrixClient(
|
||||
base_url=homeserver,
|
||||
valid_cert_check=verify_tls
|
||||
)
|
||||
_client.login_with_password(username, password)
|
||||
store_token(mx_id, _client.token)
|
||||
return _client
|
||||
except MatrixRequestError as ex:
|
||||
_LOGGER.error(
|
||||
'login_by_password: (%d) %s', ex.code, ex.content
|
||||
)
|
||||
|
||||
# this is as close as we can get to the mx_id, since there is no
|
||||
# homeserver discovery protocol we have to fall back to the homeserver url
|
||||
# instead of the actual domain it serves.
|
||||
mx_id = "{user}@{homeserver}".format(
|
||||
user=username,
|
||||
homeserver=homeserver
|
||||
)
|
||||
|
||||
if mx_id in AUTH_TOKENS:
|
||||
client = login_by_token()
|
||||
if not client:
|
||||
client = login_by_password()
|
||||
if not client:
|
||||
_LOGGER.error(
|
||||
'login failed, both token and username/password '
|
||||
'invalid'
|
||||
)
|
||||
return
|
||||
else:
|
||||
client = login_by_password()
|
||||
if not client:
|
||||
_LOGGER.error('login failed, username/password invalid')
|
||||
return
|
||||
|
||||
rooms = client.get_rooms()
|
||||
for target_room in target_rooms:
|
||||
try:
|
||||
if target_room in rooms:
|
||||
room = rooms[target_room]
|
||||
else:
|
||||
room = client.join_room(target_room)
|
||||
|
||||
_LOGGER.debug(room.send_text(message))
|
||||
except MatrixRequestError as ex:
|
||||
_LOGGER.error(
|
||||
'Unable to deliver message to room \'%s\': (%d): %s',
|
||||
target_room, ex.code, ex.content
|
||||
)
|
@ -9,14 +9,15 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA,
|
||||
BaseNotificationService)
|
||||
ATTR_DATA, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT,
|
||||
PLATFORM_SCHEMA, BaseNotificationService)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pushbullet.py==0.10.0']
|
||||
|
||||
ATTR_URL = 'url'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
@ -40,7 +41,7 @@ def get_service(hass, config):
|
||||
return PushBulletNotificationService(pushbullet)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
# pylint: disable=too-few-public-methods, too-many-branches
|
||||
class PushBulletNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for Pushbullet."""
|
||||
|
||||
@ -79,11 +80,18 @@ class PushBulletNotificationService(BaseNotificationService):
|
||||
"""
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
url = None
|
||||
if data:
|
||||
url = data.get(ATTR_URL, None)
|
||||
refreshed = False
|
||||
|
||||
if not targets:
|
||||
# Backward compatebility, notify all devices in own account
|
||||
self.pushbullet.push_note(title, message)
|
||||
if url:
|
||||
self.pushbullet.push_link(title, url, body=message)
|
||||
else:
|
||||
self.pushbullet.push_note(title, message)
|
||||
_LOGGER.info('Sent notification to self')
|
||||
return
|
||||
|
||||
@ -98,7 +106,11 @@ class PushBulletNotificationService(BaseNotificationService):
|
||||
# Target is email, send directly, don't use a target object
|
||||
# This also seems works to send to all devices in own account
|
||||
if ttype == 'email':
|
||||
self.pushbullet.push_note(title, message, email=tname)
|
||||
if url:
|
||||
self.pushbullet.push_link(title, url,
|
||||
body=message, email=tname)
|
||||
else:
|
||||
self.pushbullet.push_note(title, message, email=tname)
|
||||
_LOGGER.info('Sent notification to email %s', tname)
|
||||
continue
|
||||
|
||||
@ -117,7 +129,11 @@ class PushBulletNotificationService(BaseNotificationService):
|
||||
# Attempt push_note on a dict value. Keys are types & target
|
||||
# name. Dict pbtargets has all *actual* targets.
|
||||
try:
|
||||
self.pbtargets[ttype][tname].push_note(title, message)
|
||||
if url:
|
||||
self.pbtargets[ttype][tname].push_link(title, url,
|
||||
body=message)
|
||||
else:
|
||||
self.pbtargets[ttype][tname].push_note(title, message)
|
||||
_LOGGER.info('Sent notification to %s/%s', ttype, tname)
|
||||
except KeyError:
|
||||
_LOGGER.error('No such target: %s/%s', ttype, tname)
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.components.notify import (
|
||||
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['sendgrid==3.4.0']
|
||||
REQUIREMENTS = ['sendgrid==3.6.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -17,3 +17,15 @@ notify:
|
||||
data:
|
||||
description: Extended information for notification. Optional depending on the platform.
|
||||
example: platform specific
|
||||
|
||||
apns_register:
|
||||
description: Registers a device to receive push notifications.
|
||||
|
||||
fields:
|
||||
push_id:
|
||||
description: The device token, a 64 character hex string (256 bits). The device token is provided to you by your client app, which receives the token after registering itself with the remote notification service.
|
||||
example: '72f2a8633655c5ce574fdc9b2b34ff8abdfc3b739b6ceb7a9ff06c1cbbf99f62'
|
||||
|
||||
name:
|
||||
description: A friendly name for the device (optional).
|
||||
example: 'Sam''s iPhone'
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['slacker==0.9.25']
|
||||
REQUIREMENTS = ['slacker==0.9.28']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
109
homeassistant/components/notify/telstra.py
Normal file
109
homeassistant/components/notify/telstra.py
Normal file
@ -0,0 +1,109 @@
|
||||
"""
|
||||
Telstra API platform for notify component.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.telstra/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (BaseNotificationService,
|
||||
ATTR_TITLE,
|
||||
PLATFORM_SCHEMA)
|
||||
from homeassistant.const import CONTENT_TYPE_JSON
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_CONSUMER_KEY = 'consumer_key'
|
||||
CONF_CONSUMER_SECRET = 'consumer_secret'
|
||||
CONF_PHONE_NUMBER = 'phone_number'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_CONSUMER_KEY): cv.string,
|
||||
vol.Required(CONF_CONSUMER_SECRET): cv.string,
|
||||
vol.Required(CONF_PHONE_NUMBER): cv.string,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Get the Telstra SMS API notification service."""
|
||||
consumer_key = config.get(CONF_CONSUMER_KEY)
|
||||
consumer_secret = config.get(CONF_CONSUMER_SECRET)
|
||||
phone_number = config.get(CONF_PHONE_NUMBER)
|
||||
|
||||
# Attempt an initial authentication to confirm credentials
|
||||
if _authenticate(consumer_key, consumer_secret) is False:
|
||||
_LOGGER.exception('Error obtaining authorization from Telstra API')
|
||||
return None
|
||||
|
||||
return TelstraNotificationService(consumer_key,
|
||||
consumer_secret,
|
||||
phone_number)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods, too-many-arguments
|
||||
class TelstraNotificationService(BaseNotificationService):
|
||||
"""Implementation of a notification service for the Telstra SMS API."""
|
||||
|
||||
def __init__(self, consumer_key, consumer_secret, phone_number):
|
||||
"""Initialize the service."""
|
||||
self._consumer_key = consumer_key
|
||||
self._consumer_secret = consumer_secret
|
||||
self._phone_number = phone_number
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
title = kwargs.get(ATTR_TITLE)
|
||||
|
||||
# Retrieve authorization first
|
||||
token_response = _authenticate(self._consumer_key,
|
||||
self._consumer_secret)
|
||||
if token_response is False:
|
||||
_LOGGER.exception('Error obtaining authorization from Telstra API')
|
||||
return
|
||||
|
||||
# Send the SMS
|
||||
if title:
|
||||
text = '{} {}'.format(title, message)
|
||||
else:
|
||||
text = message
|
||||
|
||||
message_data = {
|
||||
'to': self._phone_number,
|
||||
'body': text
|
||||
}
|
||||
message_resource = 'https://api.telstra.com/v1/sms/messages'
|
||||
message_headers = {
|
||||
'Content-Type': CONTENT_TYPE_JSON,
|
||||
'Authorization': 'Bearer ' + token_response['access_token']
|
||||
}
|
||||
message_response = requests.post(message_resource,
|
||||
headers=message_headers,
|
||||
json=message_data,
|
||||
timeout=10)
|
||||
|
||||
if message_response.status_code != 202:
|
||||
_LOGGER.exception("Failed to send SMS. Status code: %d",
|
||||
message_response.status_code)
|
||||
|
||||
|
||||
def _authenticate(consumer_key, consumer_secret):
|
||||
"""Authenticate with the Telstra API."""
|
||||
token_data = {
|
||||
'client_id': consumer_key,
|
||||
'client_secret': consumer_secret,
|
||||
'grant_type': 'client_credentials',
|
||||
'scope': 'SMS'
|
||||
}
|
||||
token_resource = 'https://api.telstra.com/v1/oauth/token'
|
||||
token_response = requests.get(token_resource,
|
||||
params=token_data,
|
||||
timeout=10).json()
|
||||
|
||||
if 'error' in token_response:
|
||||
return False
|
||||
|
||||
return token_response
|
@ -14,10 +14,11 @@ from homeassistant.components.notify import (
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT
|
||||
|
||||
REQUIREMENTS = ['sleekxmpp==1.3.1',
|
||||
'dnspython3==1.14.0',
|
||||
'dnspython3==1.15.0',
|
||||
'pyasn1==0.1.9',
|
||||
'pyasn1-modules==0.0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_TLS = 'tls'
|
||||
|
||||
@ -29,9 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
"""Get the Jabber (XMPP) notification service."""
|
||||
return XmppNotificationService(
|
||||
|
@ -79,8 +79,7 @@ class NuimoThread(threading.Thread):
|
||||
self._name = name
|
||||
self._hass_is_running = True
|
||||
self._nuimo = None
|
||||
self._listener = hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
self.stop)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
|
||||
|
||||
def run(self):
|
||||
"""Setup connection or be idle."""
|
||||
@ -99,8 +98,6 @@ class NuimoThread(threading.Thread):
|
||||
"""Terminate Thread by unsetting flag."""
|
||||
_LOGGER.debug('Stopping thread for Nuimo %s', self._mac)
|
||||
self._hass_is_running = False
|
||||
self._hass.bus.remove_listener(EVENT_HOMEASSISTANT_STOP,
|
||||
self._listener)
|
||||
|
||||
def _attach(self):
|
||||
"""Create a nuimo object from mac address or discovery."""
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT,
|
||||
CONF_WHITELIST)
|
||||
|
||||
REQUIREMENTS = ['pilight==0.0.2']
|
||||
REQUIREMENTS = ['pilight==0.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -102,7 +102,7 @@ def setup(hass, config):
|
||||
if not whitelist:
|
||||
hass.bus.fire(EVENT, data)
|
||||
# Check if data matches the defined whitelist
|
||||
elif all(data[key] in whitelist[key] for key in whitelist):
|
||||
elif all(str(data[key]) in whitelist[key] for key in whitelist):
|
||||
hass.bus.fire(EVENT, data)
|
||||
|
||||
pilight_client.set_callback(handle_received_code)
|
||||
|
@ -9,106 +9,92 @@ https://home-assistant.io/components/proximity/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_ZONE, CONF_DEVICES, CONF_UNIT_OF_MEASUREMENT)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.util.location import distance
|
||||
from homeassistant.util.distance import convert
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
|
||||
|
||||
DEPENDENCIES = ['zone', 'device_tracker']
|
||||
|
||||
DOMAIN = 'proximity'
|
||||
|
||||
NOT_SET = 'not set'
|
||||
|
||||
# Default tolerance
|
||||
DEFAULT_TOLERANCE = 1
|
||||
|
||||
# Default zone
|
||||
DEFAULT_PROXIMITY_ZONE = 'home'
|
||||
|
||||
# Default distance to zone
|
||||
DEFAULT_DIST_TO_ZONE = NOT_SET
|
||||
|
||||
# Default direction of travel
|
||||
DEFAULT_DIR_OF_TRAVEL = NOT_SET
|
||||
|
||||
# Default nearest device
|
||||
DEFAULT_NEAREST = NOT_SET
|
||||
|
||||
# Entity attributes
|
||||
ATTR_DIST_FROM = 'dist_to_zone'
|
||||
ATTR_DIR_OF_TRAVEL = 'dir_of_travel'
|
||||
ATTR_NEAREST = 'nearest'
|
||||
from homeassistant.util.location import distance
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DIR_OF_TRAVEL = 'dir_of_travel'
|
||||
ATTR_DIST_FROM = 'dist_to_zone'
|
||||
ATTR_NEAREST = 'nearest'
|
||||
|
||||
def setup_proximity_component(hass, config):
|
||||
CONF_IGNORED_ZONES = 'ignored_zones'
|
||||
CONF_TOLERANCE = 'tolerance'
|
||||
|
||||
DEFAULT_DIR_OF_TRAVEL = 'not set'
|
||||
DEFAULT_DIST_TO_ZONE = 'not set'
|
||||
DEFAULT_NEAREST = 'not set'
|
||||
DEFAULT_PROXIMITY_ZONE = 'home'
|
||||
DEFAULT_TOLERANCE = 1
|
||||
DEPENDENCIES = ['zone', 'device_tracker']
|
||||
DOMAIN = 'proximity'
|
||||
|
||||
UNITS = ['km', 'm', 'mi', 'ft']
|
||||
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_ZONE, default=DEFAULT_PROXIMITY_ZONE): cv.string,
|
||||
vol.Optional(CONF_DEVICES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.entity_id]),
|
||||
vol.Optional(CONF_IGNORED_ZONES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): cv.positive_int,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): vol.All(cv.string, vol.In(UNITS)),
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: ZONE_SCHEMA,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup_proximity_component(hass, name, config):
|
||||
"""Set up individual proximity component."""
|
||||
# Get the devices from configuration.yaml.
|
||||
if 'devices' not in config:
|
||||
_LOGGER.error('devices not found in config')
|
||||
return False
|
||||
|
||||
ignored_zones = []
|
||||
if 'ignored_zones' in config:
|
||||
for variable in config['ignored_zones']:
|
||||
ignored_zones.append(variable)
|
||||
|
||||
proximity_devices = []
|
||||
for variable in config['devices']:
|
||||
proximity_devices.append(variable)
|
||||
|
||||
# Get the direction of travel tolerance from configuration.yaml.
|
||||
tolerance = config.get('tolerance', DEFAULT_TOLERANCE)
|
||||
|
||||
# Get the zone to monitor proximity to from configuration.yaml.
|
||||
proximity_zone = config.get('zone', DEFAULT_PROXIMITY_ZONE)
|
||||
|
||||
# Get the unit of measurement from configuration.yaml.
|
||||
unit_of_measure = config.get(ATTR_UNIT_OF_MEASUREMENT,
|
||||
hass.config.units.length_unit)
|
||||
|
||||
ignored_zones = config.get(CONF_IGNORED_ZONES)
|
||||
proximity_devices = config.get(CONF_DEVICES)
|
||||
tolerance = config.get(CONF_TOLERANCE)
|
||||
proximity_zone = name
|
||||
unit_of_measurement = config.get(
|
||||
CONF_UNIT_OF_MEASUREMENT, hass.config.units.length_unit)
|
||||
zone_id = 'zone.{}'.format(proximity_zone)
|
||||
state = hass.states.get(zone_id)
|
||||
zone_friendly_name = (state.name).lower()
|
||||
|
||||
proximity = Proximity(hass, zone_friendly_name, DEFAULT_DIST_TO_ZONE,
|
||||
proximity = Proximity(hass, proximity_zone, DEFAULT_DIST_TO_ZONE,
|
||||
DEFAULT_DIR_OF_TRAVEL, DEFAULT_NEAREST,
|
||||
ignored_zones, proximity_devices, tolerance,
|
||||
zone_id, unit_of_measure)
|
||||
zone_id, unit_of_measurement)
|
||||
proximity.entity_id = '{}.{}'.format(DOMAIN, proximity_zone)
|
||||
|
||||
proximity.update_ha_state()
|
||||
|
||||
# Main command to monitor proximity of devices.
|
||||
track_state_change(hass, proximity_devices,
|
||||
proximity.check_proximity_state_change)
|
||||
track_state_change(
|
||||
hass, proximity_devices, proximity.check_proximity_state_change)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Get the zones and offsets from configuration.yaml."""
|
||||
result = True
|
||||
if isinstance(config[DOMAIN], list):
|
||||
for proximity_config in config[DOMAIN]:
|
||||
if not setup_proximity_component(hass, proximity_config):
|
||||
result = False
|
||||
elif not setup_proximity_component(hass, config[DOMAIN]):
|
||||
result = False
|
||||
for zone, proximity_config in config[DOMAIN].items():
|
||||
setup_proximity_component(hass, zone, proximity_config)
|
||||
|
||||
return result
|
||||
return True
|
||||
|
||||
|
||||
class Proximity(Entity): # pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class Proximity(Entity):
|
||||
"""Representation of a Proximity."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, zone_friendly_name, dist_to, dir_of_travel,
|
||||
nearest, ignored_zones, proximity_devices, tolerance,
|
||||
proximity_zone, unit_of_measure):
|
||||
proximity_zone, unit_of_measurement):
|
||||
"""Initialize the proximity."""
|
||||
self.hass = hass
|
||||
self.friendly_name = zone_friendly_name
|
||||
@ -119,7 +105,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
|
||||
self.proximity_devices = proximity_devices
|
||||
self.tolerance = tolerance
|
||||
self.proximity_zone = proximity_zone
|
||||
self.unit_of_measure = unit_of_measure
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -134,7 +120,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity."""
|
||||
return self.unit_of_measure
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
@ -209,7 +195,7 @@ class Proximity(Entity): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
# Add the device and distance to a dictionary.
|
||||
distances_to_zone[device] = round(
|
||||
convert(dist_to_zone, 'm', self.unit_of_measure), 1)
|
||||
convert(dist_to_zone, 'm', self.unit_of_measurement), 1)
|
||||
|
||||
# Loop through each of the distances collected and work out the
|
||||
# closest.
|
||||
|
@ -7,7 +7,6 @@ to query this database.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/recorder/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import queue
|
||||
import threading
|
||||
@ -17,7 +16,7 @@ from typing import Any, Union, Optional, List
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
EVENT_TIME_CHANGED, MATCH_ALL)
|
||||
@ -26,15 +25,15 @@ from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.helpers.typing import ConfigType, QueryType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
DOMAIN = "recorder"
|
||||
DOMAIN = 'recorder'
|
||||
|
||||
REQUIREMENTS = ['sqlalchemy==1.0.15']
|
||||
REQUIREMENTS = ['sqlalchemy==1.1.1']
|
||||
|
||||
DEFAULT_URL = "sqlite:///{hass_config_path}"
|
||||
DEFAULT_DB_FILE = "home-assistant_v2.db"
|
||||
DEFAULT_URL = 'sqlite:///{hass_config_path}'
|
||||
DEFAULT_DB_FILE = 'home-assistant_v2.db'
|
||||
|
||||
CONF_DB_URL = "db_url"
|
||||
CONF_PURGE_DAYS = "purge_days"
|
||||
CONF_DB_URL = 'db_url'
|
||||
CONF_PURGE_DAYS = 'purge_days'
|
||||
|
||||
RETRIES = 3
|
||||
CONNECT_RETRY_WAIT = 10
|
||||
@ -56,8 +55,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
Session = None # pylint: disable=no-member
|
||||
|
||||
|
||||
def execute(q: QueryType) \
|
||||
-> List[Any]: # pylint: disable=invalid-sequence-index
|
||||
# pylint: disable=invalid-sequence-index
|
||||
def execute(q: QueryType) -> List[Any]:
|
||||
"""Query the database and convert the objects to HA native form.
|
||||
|
||||
This method also retries a few times in the case of stale connections.
|
||||
@ -101,7 +100,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
global _INSTANCE # pylint: disable=global-statement
|
||||
|
||||
if _INSTANCE is not None:
|
||||
_LOGGER.error('Only a single instance allowed.')
|
||||
_LOGGER.error("Only a single instance allowed")
|
||||
return False
|
||||
|
||||
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
|
||||
@ -155,8 +154,7 @@ class Recorder(threading.Thread):
|
||||
"""A threaded recorder class."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, hass: HomeAssistant, purge_days: int, uri: str) \
|
||||
-> None:
|
||||
def __init__(self, hass: HomeAssistant, purge_days: int, uri: str) -> None:
|
||||
"""Initialize the recorder."""
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
@ -226,7 +224,7 @@ class Recorder(threading.Thread):
|
||||
|
||||
self.queue.task_done()
|
||||
|
||||
@asyncio.coroutine
|
||||
@callback
|
||||
def event_listener(self, event):
|
||||
"""Listen for new events and put them in the process queue."""
|
||||
self.queue.put(event)
|
||||
|
@ -11,8 +11,9 @@ from homeassistant.helpers.entity import generate_entity_id
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/sander76/powerviewApi/'
|
||||
'archive/cc6f75dd39160d4aaf46cb2ed9220136b924bcb4.zip#powerviewApi==0.2']
|
||||
'https://github.com/sander76/powerviewApi/archive'
|
||||
'/246e782d60d5c0addcc98d7899a0186f9d5640b0.zip#powerviewApi==0.3.15'
|
||||
]
|
||||
|
||||
HUB_ADDRESS = 'address'
|
||||
|
||||
@ -20,7 +21,7 @@ HUB_ADDRESS = 'address'
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the powerview scenes stored in a Powerview hub."""
|
||||
import powerview
|
||||
from powerview_api import powerview
|
||||
|
||||
hub_address = config.get(HUB_ADDRESS)
|
||||
|
||||
|
@ -8,28 +8,44 @@ https://home-assistant.io/components/sensor.arduino/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
import homeassistant.components.arduino as arduino
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PINS = 'pins'
|
||||
CONF_TYPE = 'analog'
|
||||
|
||||
DEPENDENCIES = ['arduino']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PIN_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PINS):
|
||||
vol.Schema({cv.positive_int: PIN_SCHEMA}),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Arduino platform."""
|
||||
"""Set up the Arduino platform."""
|
||||
# Verify that the Arduino board is present
|
||||
if arduino.BOARD is None:
|
||||
_LOGGER.error('A connection has not been made to the Arduino board.')
|
||||
_LOGGER.error("A connection has not been made to the Arduino board")
|
||||
return False
|
||||
|
||||
pins = config.get(CONF_PINS)
|
||||
|
||||
sensors = []
|
||||
pins = config.get('pins')
|
||||
for pinnum, pin in pins.items():
|
||||
if pin.get('name'):
|
||||
sensors.append(ArduinoSensor(pin.get('name'),
|
||||
pinnum,
|
||||
'analog'))
|
||||
sensors.append(ArduinoSensor(pin.get(CONF_NAME), pinnum, CONF_TYPE))
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
@ -39,7 +55,7 @@ class ArduinoSensor(Entity):
|
||||
def __init__(self, name, pin, pin_type):
|
||||
"""Initialize the sensor."""
|
||||
self._pin = pin
|
||||
self._name = name or DEVICE_DEFAULT_NAME
|
||||
self._name = name
|
||||
self.pin_type = pin_type
|
||||
self.direction = 'in'
|
||||
self._value = None
|
||||
|
126
homeassistant/components/sensor/arwn.py
Normal file
126
homeassistant/components/sensor/arwn.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""Support for collecting data from the ARWN project.
|
||||
|
||||
For more details about this platform, please refer to the
|
||||
documentation at https://home-assistant.io/components/sensor.arwn/
|
||||
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
DOMAIN = "arwn"
|
||||
TOPIC = 'arwn/#'
|
||||
SENSORS = {}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def discover_sensors(topic, payload):
|
||||
"""Given a topic, dynamically create the right sensor type."""
|
||||
parts = topic.split('/')
|
||||
unit = payload.get('units', '')
|
||||
domain = parts[1]
|
||||
if domain == "temperature":
|
||||
name = parts[2]
|
||||
if unit == "F":
|
||||
unit = TEMP_FAHRENHEIT
|
||||
else:
|
||||
unit = TEMP_CELSIUS
|
||||
return (ArwnSensor(name, 'temp', unit),)
|
||||
if domain == "barometer":
|
||||
return (ArwnSensor("Barometer", 'pressure', unit),)
|
||||
if domain == "wind":
|
||||
return (ArwnSensor("Wind Speed", 'speed', unit),
|
||||
ArwnSensor("Wind Gust", 'gust', unit),
|
||||
ArwnSensor("Wind Direction", 'direction', '°'))
|
||||
|
||||
|
||||
def _slug(name):
|
||||
return "sensor.arwn_%s" % slugify(name)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the ARWN platform."""
|
||||
def sensor_event_received(topic, payload, qos):
|
||||
"""Process events as sensors.
|
||||
|
||||
When a new event on our topic (arwn/#) is received we map it
|
||||
into a known kind of sensor based on topic name. If we've
|
||||
never seen this before, we keep this sensor around in a global
|
||||
cache. If we have seen it before, we update the values of the
|
||||
existing sensor. Either way, we push an ha state update at the
|
||||
end for the new event we've seen.
|
||||
|
||||
This lets us dynamically incorporate sensors without any
|
||||
configuration on our side.
|
||||
"""
|
||||
event = json.loads(payload)
|
||||
sensors = discover_sensors(topic, event)
|
||||
if not sensors:
|
||||
return
|
||||
|
||||
if 'timestamp' in event:
|
||||
del event['timestamp']
|
||||
|
||||
for sensor in sensors:
|
||||
if sensor.name not in SENSORS:
|
||||
sensor.hass = hass
|
||||
sensor.set_event(event)
|
||||
SENSORS[sensor.name] = sensor
|
||||
_LOGGER.debug("Registering new sensor %(name)s => %(event)s",
|
||||
dict(name=sensor.name, event=event))
|
||||
add_devices((sensor,))
|
||||
else:
|
||||
SENSORS[sensor.name].set_event(event)
|
||||
SENSORS[sensor.name].update_ha_state()
|
||||
|
||||
mqtt.subscribe(hass, TOPIC, sensor_event_received, 0)
|
||||
return True
|
||||
|
||||
|
||||
class ArwnSensor(Entity):
|
||||
"""Represents an ARWN sensor."""
|
||||
|
||||
def __init__(self, name, state_key, units):
|
||||
"""Initialize the sensor."""
|
||||
self.hass = None
|
||||
self.entity_id = _slug(name)
|
||||
self._name = name
|
||||
self._state_key = state_key
|
||||
self.event = {}
|
||||
self._unit_of_measurement = units
|
||||
|
||||
def set_event(self, event):
|
||||
"""Update the sensor with the most recent event."""
|
||||
self.event = {}
|
||||
self.event.update(event)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self.event.get(self._state_key, None)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Get the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return all the state attributes."""
|
||||
return self.event
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Unit this state is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Should we poll."""
|
||||
return False
|
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