Merge pull request #3937 from home-assistant/dev

0.31
This commit is contained in:
Robbie Trencheny 2016-10-22 15:22:07 -07:00 committed by GitHub
commit ef2ed7bfc9
231 changed files with 10502 additions and 3274 deletions

View File

@ -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

View File

@ -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**

View File

@ -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

View File

@ -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:

View File

@ -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
-------------------------------

View File

@ -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:

View File

@ -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: {}->{}.'\

View 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()

View File

@ -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."""

View File

@ -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)

View File

@ -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

View 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)

View 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

View File

@ -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))

View File

@ -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,)

View File

@ -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:

View File

@ -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

View 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

View 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

View File

@ -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.

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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):

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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):

View 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

View File

@ -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

View File

@ -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},
})

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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'),

View 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

View File

@ -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))

View File

@ -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')

View File

@ -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

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit db109f5dda043182a7e9647b161851e83be9b91e
Subproject commit f3081ed48fd11fa89586701dba3792d028473a15

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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,

View File

@ -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()

View 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"})

View File

@ -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__)

View File

@ -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):

View File

@ -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__)

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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."""

View File

@ -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

View File

@ -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

View File

@ -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."""

View File

@ -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({

View File

@ -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'

View File

@ -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)

View 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

View 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])

View 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
)

View File

@ -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)

View File

@ -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__)

View File

@ -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'

View File

@ -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__)

View 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

View File

@ -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(

View File

@ -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."""

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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

View 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