Merge pull request #14876 from home-assistant/rc

0.71.0
This commit is contained in:
Paulus Schoutsen 2018-06-08 18:07:39 -04:00 committed by GitHub
commit 8ceb57752b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
169 changed files with 4220 additions and 2023 deletions

View File

@ -123,6 +123,9 @@ omit =
homeassistant/components/homematicip_cloud.py homeassistant/components/homematicip_cloud.py
homeassistant/components/*/homematicip_cloud.py homeassistant/components/*/homematicip_cloud.py
homeassistant/components/hydrawise.py
homeassistant/components/*/hydrawise.py
homeassistant/components/ihc/* homeassistant/components/ihc/*
homeassistant/components/*/ihc.py homeassistant/components/*/ihc.py
@ -216,7 +219,7 @@ omit =
homeassistant/components/raincloud.py homeassistant/components/raincloud.py
homeassistant/components/*/raincloud.py homeassistant/components/*/raincloud.py
homeassistant/components/rainmachine.py homeassistant/components/rainmachine/*
homeassistant/components/*/rainmachine.py homeassistant/components/*/rainmachine.py
homeassistant/components/raspihats.py homeassistant/components/raspihats.py
@ -382,6 +385,7 @@ omit =
homeassistant/components/cover/myq.py homeassistant/components/cover/myq.py
homeassistant/components/cover/opengarage.py homeassistant/components/cover/opengarage.py
homeassistant/components/cover/rpi_gpio.py homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/ryobi_gdo.py
homeassistant/components/cover/scsgate.py homeassistant/components/cover/scsgate.py
homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/aruba.py
@ -443,6 +447,7 @@ omit =
homeassistant/components/light/lifx_legacy.py homeassistant/components/light/lifx_legacy.py
homeassistant/components/light/lifx.py homeassistant/components/light/lifx.py
homeassistant/components/light/limitlessled.py homeassistant/components/light/limitlessled.py
homeassistant/components/light/lw12wifi.py
homeassistant/components/light/mystrom.py homeassistant/components/light/mystrom.py
homeassistant/components/light/nanoleaf_aurora.py homeassistant/components/light/nanoleaf_aurora.py
homeassistant/components/light/osramlightify.py homeassistant/components/light/osramlightify.py
@ -518,9 +523,10 @@ omit =
homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/ciscospark.py homeassistant/components/notify/ciscospark.py
homeassistant/components/notify/clickatell.py homeassistant/components/notify/clickatell.py
homeassistant/components/notify/clicksend_tts.py
homeassistant/components/notify/clicksend.py homeassistant/components/notify/clicksend.py
homeassistant/components/notify/clicksend_tts.py
homeassistant/components/notify/discord.py homeassistant/components/notify/discord.py
homeassistant/components/notify/flock.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py homeassistant/components/notify/gntp.py
homeassistant/components/notify/group.py homeassistant/components/notify/group.py
@ -533,7 +539,6 @@ omit =
homeassistant/components/notify/message_bird.py homeassistant/components/notify/message_bird.py
homeassistant/components/notify/mycroft.py homeassistant/components/notify/mycroft.py
homeassistant/components/notify/nfandroidtv.py homeassistant/components/notify/nfandroidtv.py
homeassistant/components/notify/nma.py
homeassistant/components/notify/prowl.py homeassistant/components/notify/prowl.py
homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushetta.py homeassistant/components/notify/pushetta.py
@ -620,6 +625,7 @@ omit =
homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap.py
homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/influxdb.py
homeassistant/components/sensor/iperf3.py
homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/irish_rail_transport.py
homeassistant/components/sensor/kwb.py homeassistant/components/sensor/kwb.py
homeassistant/components/sensor/lacrosse.py homeassistant/components/sensor/lacrosse.py

View File

@ -20,7 +20,7 @@ If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools: If the code communicates with devices, web services, or third-party tools:
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
- [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`.
- [ ] New files were added to `.coveragerc`. - [ ] New files were added to `.coveragerc`.
If the code does not interact with devices: If the code does not interact with devices:

View File

@ -78,7 +78,6 @@ homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/upnp.py @dgomes homeassistant/components/sensor/upnp.py @dgomes
homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya
homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/vacuum/roomba.py @pschmitt
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
@ -100,6 +99,8 @@ homeassistant/components/matrix.py @tinloaf
homeassistant/components/*/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf
homeassistant/components/qwikswitch.py @kellerza homeassistant/components/qwikswitch.py @kellerza
homeassistant/components/*/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza
homeassistant/components/rainmachine/* @bachya
homeassistant/components/*/rainmachine.py @bachya
homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/tahoma.py @philklei homeassistant/components/tahoma.py @philklei
homeassistant/components/*/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei

View File

@ -12,6 +12,7 @@ LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
#ENV INSTALL_LIBCEC no #ENV INSTALL_LIBCEC no
#ENV INSTALL_PHANTOMJS no #ENV INSTALL_PHANTOMJS no
#ENV INSTALL_SSOCR no #ENV INSTALL_SSOCR no
#ENV INSTALL_IPERF3 no
VOLUME /config VOLUME /config

View File

@ -1,606 +0,0 @@
swagger: '2.0'
info:
title: Home Assistant
description: Home Assistant REST API
version: "1.0.1"
# the domain of the service
host: localhost:8123
# array of all schemes that your API supports
schemes:
- http
- https
securityDefinitions:
#api_key:
# type: apiKey
# description: API password
# 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
consumes:
- application/json
produces:
- application/json
paths:
/:
get:
summary: API alive message
description: Returns message if API is up and running.
tags:
- Core
security:
- api_key: []
responses:
200:
description: API is up and running
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/config:
get:
summary: API alive message
description: Returns the current configuration as JSON.
tags:
- Core
security:
- api_key: []
responses:
200:
description: Current configuration
schema:
$ref: '#/definitions/ApiConfig'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/discovery_info:
get:
summary: Basic information about Home Assistant instance
tags:
- Core
responses:
200:
description: Basic information
schema:
$ref: '#/definitions/DiscoveryInfo'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/bootstrap:
get:
summary: Returns all data needed to bootstrap Home Assistant.
tags:
- Core
security:
- api_key: []
responses:
200:
description: Bootstrap information
schema:
$ref: '#/definitions/BootstrapInfo'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/events:
get:
summary: Array of event objects.
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
schema:
type: array
items:
$ref: '#/definitions/Event'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/services:
get:
summary: Array of service objects.
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
schema:
type: array
items:
$ref: '#/definitions/Service'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/history:
get:
summary: Array of state changes in the past.
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
schema:
type: array
items:
$ref: '#/definitions/History'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/states:
get:
summary: Array of state objects.
description: |
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
schema:
type: array
items:
$ref: '#/definitions/State'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/states/{entity_id}:
get:
summary: Specific state object.
description: |
Returns a state object for specified entity_id.
tags:
- State
security:
- api_key: []
parameters:
- name: entity_id
in: path
description: entity_id of the entity to query
required: true
type: string
responses:
200:
description: State
schema:
$ref: '#/definitions/State'
404:
description: Not found
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
post:
description: |
Updates or creates the current state of an entity.
tags:
- State
consumes:
- application/json
parameters:
- name: entity_id
in: path
description: entity_id to set the state of
required: true
type: string
- $ref: '#/parameters/State'
responses:
200:
description: State of existing entity was set
schema:
$ref: '#/definitions/State'
201:
description: State of new entity was set
schema:
$ref: '#/definitions/State'
headers:
location:
type: string
description: location of the new entity
default:
description: Error
schema:
$ref: '#/definitions/Message'
/error_log:
get:
summary: Error log
description: |
Retrieve all errors logged during the current session of Home Assistant as a plaintext response.
tags:
- Core
security:
- api_key: []
produces:
- text/plain
responses:
200:
description: Plain text error log
default:
description: Error
schema:
$ref: '#/definitions/Message'
/camera_proxy/camera.{entity_id}:
get:
summary: Camera image.
description: |
Returns the data (image) from the specified camera entity_id.
tags:
- Camera
security:
- api_key: []
produces:
- image/jpeg
parameters:
- name: entity_id
in: path
description: entity_id of the camera to query
required: true
type: string
responses:
200:
description: Camera image
schema:
type: file
default:
description: Error
schema:
$ref: '#/definitions/Message'
/events/{event_type}:
post:
description: |
Fires an event with event_type
tags:
- Events
security:
- api_key: []
consumes:
- application/json
parameters:
- name: event_type
in: path
description: event_type to fire event with
required: true
type: string
- $ref: '#/parameters/EventData'
responses:
200:
description: Response message
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/services/{domain}/{service}:
post:
description: |
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:
- name: domain
in: path
description: domain of the service
required: true
type: string
- name: service
in: path
description: service to call
required: true
type: string
- $ref: '#/parameters/ServiceData'
responses:
200:
description: List of states that have changed while the service was being executed. The result will include any changed states that changed while the service was being executed, even if their change was the result of something else happening in the system.
schema:
type: array
items:
$ref: '#/definitions/State'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/template:
post:
description: |
Render a Home Assistant template.
tags:
- Template
security:
- api_key: []
consumes:
- application/json
produces:
- text/plain
parameters:
- $ref: '#/parameters/Template'
responses:
200:
description: Returns the rendered template in plain text.
schema:
type: string
default:
description: Error
schema:
$ref: '#/definitions/Message'
/event_forwarding:
post:
description: |
Setup event forwarding to another Home Assistant instance.
tags:
- Core
security:
- api_key: []
consumes:
- application/json
parameters:
- $ref: '#/parameters/EventForwarding'
responses:
200:
description: It will return a message if event forwarding was setup successful.
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
delete:
description: |
Cancel event forwarding to another Home Assistant instance.
tags:
- Core
consumes:
- application/json
parameters:
- $ref: '#/parameters/EventForwarding'
responses:
200:
description: It will return a message if event forwarding was cancelled successful.
schema:
$ref: '#/definitions/Message'
default:
description: Error
schema:
$ref: '#/definitions/Message'
/stream:
get:
summary: Server-sent events
description: The server-sent events feature is a one-way channel from your Home Assistant server to a client which is acting as a consumer.
tags:
- Core
- Events
security:
- api_key: []
produces:
- text/event-stream
parameters:
- name: restrict
in: query
description: comma-separated list of event_types to filter
required: false
type: string
responses:
default:
description: Stream of events
schema:
type: object
x-events:
state_changed:
type: object
properties:
entity_id:
type: string
old_state:
$ref: '#/definitions/State'
new_state:
$ref: '#/definitions/State'
definitions:
ApiConfig:
type: object
properties:
components:
type: array
description: List of component types
items:
type: string
description: Component type
latitude:
type: number
format: float
description: Latitude of Home Assistant server
longitude:
type: number
format: float
description: Longitude of Home Assistant server
location_name:
type: string
unit_system:
type: object
properties:
length:
type: string
mass:
type: string
temperature:
type: string
volume:
type: string
time_zone:
type: string
version:
type: string
DiscoveryInfo:
type: object
properties:
base_url:
type: string
location_name:
type: string
requires_api_password:
type: boolean
version:
type: string
BootstrapInfo:
type: object
properties:
config:
$ref: '#/definitions/ApiConfig'
events:
type: array
items:
$ref: '#/definitions/Event'
services:
type: array
items:
$ref: '#/definitions/Service'
states:
type: array
items:
$ref: '#/definitions/State'
Event:
type: object
properties:
event:
type: string
listener_count:
type: integer
Service:
type: object
properties:
domain:
type: string
services:
type: object
additionalProperties:
$ref: '#/definitions/DomainService'
DomainService:
type: object
properties:
description:
type: string
fields:
type: object
description: Object with service fields that can be called
State:
type: object
properties:
attributes:
$ref: '#/definitions/StateAttributes'
state:
type: string
entity_id:
type: string
last_changed:
type: string
format: date-time
StateAttributes:
type: object
additionalProperties:
type: string
History:
allOf:
- $ref: '#/definitions/State'
- type: object
properties:
last_updated:
type: string
format: date-time
Message:
type: object
properties:
message:
type: string
parameters:
State:
name: body
in: body
description: State parameter
required: false
schema:
type: object
required:
- state
properties:
attributes:
$ref: '#/definitions/StateAttributes'
state:
type: string
EventData:
name: body
in: body
description: event_data
required: false
schema:
type: object
ServiceData:
name: body
in: body
description: service_data
required: false
schema:
type: object
Template:
name: body
in: body
description: Template to render
required: true
schema:
type: object
required:
- template
properties:
template:
description: Jinja2 template string
type: string
EventForwarding:
name: body
in: body
description: Event Forwarding parameter
required: true
schema:
type: object
required:
- host
- api_password
properties:
host:
type: string
api_password:
type: string
port:
type: integer

View File

@ -100,8 +100,8 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the regex for code format or None if no code is required.""" """Return one or more digits/characters."""
return '^\\d{4,6}$' return 'Number'
@property @property
def state(self): def state(self):

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.alarmdotcom/
""" """
import asyncio import asyncio
import logging import logging
import re
import voluptuous as vol import voluptuous as vol
@ -79,8 +80,12 @@ class AlarmDotCom(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more characters if code is defined.""" """Return one or more digits/characters."""
return None if self._code is None else '.+' if self._code is None:
return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
@property @property
def state(self): def state(self):

View File

@ -80,7 +80,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the characters if code is defined.""" """Return the characters if code is defined."""
return '[0-9]{4}([0-9]{2})?' return 'Number'
@property @property
def state(self): def state(self):

View File

@ -106,7 +106,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
"""Regex for code format or None if no code is required.""" """Regex for code format or None if no code is required."""
if self._code: if self._code:
return None return None
return '^\\d{4,6}$' return 'Number'
@property @property
def state(self): def state(self):

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.ifttt/ https://home-assistant.io/components/alarm_control_panel.ifttt/
""" """
import logging import logging
import re
import voluptuous as vol import voluptuous as vol
@ -124,8 +125,12 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more characters.""" """Return one or more digits/characters."""
return None if self._code is None else '.+' if self._code is None:
return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/alarm_control_panel.manual/
import copy import copy
import datetime import datetime
import logging import logging
import re
import voluptuous as vol import voluptuous as vol
@ -201,8 +202,12 @@ class ManualAlarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more characters.""" """Return one or more digits/characters."""
return None if self._code is None else '.+' if self._code is None:
return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -8,6 +8,7 @@ import asyncio
import copy import copy
import datetime import datetime
import logging import logging
import re
import voluptuous as vol import voluptuous as vol
@ -237,8 +238,12 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more characters.""" """Return one or more digits/characters."""
return None if self._code is None else '.+' if self._code is None:
return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.mqtt/
""" """
import asyncio import asyncio
import logging import logging
import re
import voluptuous as vol import voluptuous as vol
@ -117,8 +118,12 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""One or more characters if code is defined.""" """Return one or more digits/characters."""
return None if self._code is None else '.+' if self._code is None:
return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
@asyncio.coroutine @asyncio.coroutine
def async_alarm_disarm(self, code=None): def async_alarm_disarm(self, code=None):

View File

@ -69,8 +69,8 @@ class NX584Alarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return che characters if code is defined.""" """Return one or more digits/characters."""
return '[0-9]{4}([0-9]{2})?' return 'Number'
@property @property
def state(self): def state(self):

View File

@ -66,7 +66,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the regex for code format or None if no code is required.""" """Return the regex for code format or None if no code is required."""
return '^\\d{4,6}$' return 'Number'
@property @property
def state(self): def state(self):

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.simplisafe/ https://home-assistant.io/components/alarm_control_panel.simplisafe/
""" """
import logging import logging
import re
import voluptuous as vol import voluptuous as vol
@ -83,8 +84,12 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more characters if code is defined.""" """Return one or more digits/characters."""
return None if self._code is None else '.+' if self._code is None:
return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
@property @property
def state(self): def state(self):

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_CUSTOM_BYPASS) STATE_ALARM_ARMED_CUSTOM_BYPASS)
REQUIREMENTS = ['total_connect_client==0.17'] REQUIREMENTS = ['total_connect_client==0.18']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -60,8 +60,8 @@ class VerisureAlarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the code format as regex.""" """Return one or more digits/characters."""
return '^\\d{%s}$' % self._digits return 'Number'
@property @property
def changed_by(self): def changed_by(self):

View File

@ -2,7 +2,7 @@
Rest API for Home Assistant. Rest API for Home Assistant.
For more details about the RESTful API, please refer to the documentation at For more details about the RESTful API, please refer to the documentation at
https://home-assistant.io/developers/api/ https://developers.home-assistant.io/docs/en/external_api_rest.html
""" """
import asyncio import asyncio
import json import json
@ -11,31 +11,34 @@ import logging
from aiohttp import web from aiohttp import web
import async_timeout import async_timeout
import homeassistant.core as ha
import homeassistant.remote as rem
from homeassistant.bootstrap import DATA_LOGGING from homeassistant.bootstrap import DATA_LOGGING
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
MATCH_ALL, URL_API, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
URL_API_EVENTS, URL_API_SERVICES,
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
__version__)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import AsyncTrackStates
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers import template
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST,
HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS,
URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM,
URL_API_TEMPLATE, __version__)
import homeassistant.core as ha
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.state import AsyncTrackStates
import homeassistant.remote as rem
_LOGGER = logging.getLogger(__name__)
ATTR_BASE_URL = 'base_url'
ATTR_LOCATION_NAME = 'location_name'
ATTR_REQUIRES_API_PASSWORD = 'requires_api_password'
ATTR_VERSION = 'version'
DOMAIN = 'api' DOMAIN = 'api'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
STREAM_PING_PAYLOAD = "ping" STREAM_PING_PAYLOAD = 'ping'
STREAM_PING_INTERVAL = 50 # seconds STREAM_PING_INTERVAL = 50 # seconds
_LOGGER = logging.getLogger(__name__)
def setup(hass, config): def setup(hass, config):
"""Register the API with the HTTP interface.""" """Register the API with the HTTP interface."""
@ -62,19 +65,19 @@ class APIStatusView(HomeAssistantView):
"""View to handle Status requests.""" """View to handle Status requests."""
url = URL_API url = URL_API
name = "api:status" name = 'api:status'
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Retrieve if API is running.""" """Retrieve if API is running."""
return self.json_message('API running.') return self.json_message("API running.")
class APIEventStream(HomeAssistantView): class APIEventStream(HomeAssistantView):
"""View to handle EventStream requests.""" """View to handle EventStream requests."""
url = URL_API_STREAM url = URL_API_STREAM
name = "api:stream" name = 'api:stream'
async def get(self, request): async def get(self, request):
"""Provide a streaming interface for the event bus.""" """Provide a streaming interface for the event bus."""
@ -95,7 +98,7 @@ class APIEventStream(HomeAssistantView):
if restrict and event.event_type not in restrict: if restrict and event.event_type not in restrict:
return return
_LOGGER.debug('STREAM %s FORWARDING %s', id(stop_obj), event) _LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event)
if event.event_type == EVENT_HOMEASSISTANT_STOP: if event.event_type == EVENT_HOMEASSISTANT_STOP:
data = stop_obj data = stop_obj
@ -111,7 +114,7 @@ class APIEventStream(HomeAssistantView):
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
try: try:
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) _LOGGER.debug("STREAM %s ATTACHED", id(stop_obj))
# Fire off one message so browsers fire open event right away # Fire off one message so browsers fire open event right away
await to_write.put(STREAM_PING_PAYLOAD) await to_write.put(STREAM_PING_PAYLOAD)
@ -126,25 +129,25 @@ class APIEventStream(HomeAssistantView):
break break
msg = "data: {}\n\n".format(payload) msg = "data: {}\n\n".format(payload)
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), _LOGGER.debug(
msg.strip()) "STREAM %s WRITING %s", id(stop_obj), msg.strip())
await response.write(msg.encode("UTF-8")) await response.write(msg.encode('UTF-8'))
except asyncio.TimeoutError: except asyncio.TimeoutError:
await to_write.put(STREAM_PING_PAYLOAD) await to_write.put(STREAM_PING_PAYLOAD)
except asyncio.CancelledError: except asyncio.CancelledError:
_LOGGER.debug('STREAM %s ABORT', id(stop_obj)) _LOGGER.debug("STREAM %s ABORT", id(stop_obj))
finally: finally:
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) _LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj))
unsub_stream() unsub_stream()
class APIConfigView(HomeAssistantView): class APIConfigView(HomeAssistantView):
"""View to handle Config requests.""" """View to handle Configuration requests."""
url = URL_API_CONFIG url = URL_API_CONFIG
name = "api:config" name = 'api:config'
@ha.callback @ha.callback
def get(self, request): def get(self, request):
@ -153,22 +156,22 @@ class APIConfigView(HomeAssistantView):
class APIDiscoveryView(HomeAssistantView): class APIDiscoveryView(HomeAssistantView):
"""View to provide discovery info.""" """View to provide Discovery information."""
requires_auth = False requires_auth = False
url = URL_API_DISCOVERY_INFO url = URL_API_DISCOVERY_INFO
name = "api:discovery" name = 'api:discovery'
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get discovery info.""" """Get discovery information."""
hass = request.app['hass'] hass = request.app['hass']
needs_auth = hass.config.api.api_password is not None needs_auth = hass.config.api.api_password is not None
return self.json({ return self.json({
'base_url': hass.config.api.base_url, ATTR_BASE_URL: hass.config.api.base_url,
'location_name': hass.config.location_name, ATTR_LOCATION_NAME: hass.config.location_name,
'requires_api_password': needs_auth, ATTR_REQUIRES_API_PASSWORD: needs_auth,
'version': __version__ ATTR_VERSION: __version__,
}) })
@ -187,8 +190,8 @@ class APIStatesView(HomeAssistantView):
class APIEntityStateView(HomeAssistantView): class APIEntityStateView(HomeAssistantView):
"""View to handle EntityState requests.""" """View to handle EntityState requests."""
url = "/api/states/{entity_id}" url = '/api/states/{entity_id}'
name = "api:entity-state" name = 'api:entity-state'
@ha.callback @ha.callback
def get(self, request, entity_id): def get(self, request, entity_id):
@ -196,7 +199,7 @@ class APIEntityStateView(HomeAssistantView):
state = request.app['hass'].states.get(entity_id) state = request.app['hass'].states.get(entity_id)
if state: if state:
return self.json(state) return self.json(state)
return self.json_message('Entity not found', HTTP_NOT_FOUND) return self.json_message("Entity not found.", HTTP_NOT_FOUND)
async def post(self, request, entity_id): async def post(self, request, entity_id):
"""Update state of entity.""" """Update state of entity."""
@ -204,13 +207,13 @@ class APIEntityStateView(HomeAssistantView):
try: try:
data = await request.json() data = await request.json()
except ValueError: except ValueError:
return self.json_message('Invalid JSON specified', return self.json_message(
HTTP_BAD_REQUEST) "Invalid JSON specified.", HTTP_BAD_REQUEST)
new_state = data.get('state') new_state = data.get('state')
if new_state is None: if new_state is None:
return self.json_message('No state specified', HTTP_BAD_REQUEST) return self.json_message("No state specified.", HTTP_BAD_REQUEST)
attributes = data.get('attributes') attributes = data.get('attributes')
force_update = data.get('force_update', False) force_update = data.get('force_update', False)
@ -232,15 +235,15 @@ class APIEntityStateView(HomeAssistantView):
def delete(self, request, entity_id): def delete(self, request, entity_id):
"""Remove entity.""" """Remove entity."""
if request.app['hass'].states.async_remove(entity_id): if request.app['hass'].states.async_remove(entity_id):
return self.json_message('Entity removed') return self.json_message("Entity removed.")
return self.json_message('Entity not found', HTTP_NOT_FOUND) return self.json_message("Entity not found.", HTTP_NOT_FOUND)
class APIEventListenersView(HomeAssistantView): class APIEventListenersView(HomeAssistantView):
"""View to handle EventListeners requests.""" """View to handle EventListeners requests."""
url = URL_API_EVENTS url = URL_API_EVENTS
name = "api:event-listeners" name = 'api:event-listeners'
@ha.callback @ha.callback
def get(self, request): def get(self, request):
@ -252,7 +255,7 @@ class APIEventView(HomeAssistantView):
"""View to handle Event requests.""" """View to handle Event requests."""
url = '/api/events/{event_type}' url = '/api/events/{event_type}'
name = "api:event" name = 'api:event'
async def post(self, request, event_type): async def post(self, request, event_type):
"""Fire events.""" """Fire events."""
@ -260,12 +263,12 @@ class APIEventView(HomeAssistantView):
try: try:
event_data = json.loads(body) if body else None event_data = json.loads(body) if body else None
except ValueError: except ValueError:
return self.json_message('Event data should be valid JSON', return self.json_message(
HTTP_BAD_REQUEST) "Event data should be valid JSON.", HTTP_BAD_REQUEST)
if event_data is not None and not isinstance(event_data, dict): if event_data is not None and not isinstance(event_data, dict):
return self.json_message('Event data should be a JSON object', return self.json_message(
HTTP_BAD_REQUEST) "Event data should be a JSON object", HTTP_BAD_REQUEST)
# Special case handling for event STATE_CHANGED # Special case handling for event STATE_CHANGED
# We will try to convert state dicts back to State objects # We will try to convert state dicts back to State objects
@ -276,8 +279,8 @@ class APIEventView(HomeAssistantView):
if state: if state:
event_data[key] = state event_data[key] = state
request.app['hass'].bus.async_fire(event_type, event_data, request.app['hass'].bus.async_fire(
ha.EventOrigin.remote) event_type, event_data, ha.EventOrigin.remote)
return self.json_message("Event {} fired.".format(event_type)) return self.json_message("Event {} fired.".format(event_type))
@ -286,7 +289,7 @@ class APIServicesView(HomeAssistantView):
"""View to handle Services requests.""" """View to handle Services requests."""
url = URL_API_SERVICES url = URL_API_SERVICES
name = "api:services" name = 'api:services'
async def get(self, request): async def get(self, request):
"""Get registered services.""" """Get registered services."""
@ -297,8 +300,8 @@ class APIServicesView(HomeAssistantView):
class APIDomainServicesView(HomeAssistantView): class APIDomainServicesView(HomeAssistantView):
"""View to handle DomainServices requests.""" """View to handle DomainServices requests."""
url = "/api/services/{domain}/{service}" url = '/api/services/{domain}/{service}'
name = "api:domain-services" name = 'api:domain-services'
async def post(self, request, domain, service): async def post(self, request, domain, service):
"""Call a service. """Call a service.
@ -310,8 +313,8 @@ class APIDomainServicesView(HomeAssistantView):
try: try:
data = json.loads(body) if body else None data = json.loads(body) if body else None
except ValueError: except ValueError:
return self.json_message('Data should be valid JSON', return self.json_message(
HTTP_BAD_REQUEST) "Data should be valid JSON.", HTTP_BAD_REQUEST)
with AsyncTrackStates(hass) as changed_states: with AsyncTrackStates(hass) as changed_states:
await hass.services.async_call(domain, service, data, True) await hass.services.async_call(domain, service, data, True)
@ -323,7 +326,7 @@ class APIComponentsView(HomeAssistantView):
"""View to handle Components requests.""" """View to handle Components requests."""
url = URL_API_COMPONENTS url = URL_API_COMPONENTS
name = "api:components" name = 'api:components'
@ha.callback @ha.callback
def get(self, request): def get(self, request):
@ -332,10 +335,10 @@ class APIComponentsView(HomeAssistantView):
class APITemplateView(HomeAssistantView): class APITemplateView(HomeAssistantView):
"""View to handle requests.""" """View to handle Template requests."""
url = URL_API_TEMPLATE url = URL_API_TEMPLATE
name = "api:template" name = 'api:template'
async def post(self, request): async def post(self, request):
"""Render a template.""" """Render a template."""
@ -344,30 +347,29 @@ class APITemplateView(HomeAssistantView):
tpl = template.Template(data['template'], request.app['hass']) tpl = template.Template(data['template'], request.app['hass'])
return tpl.async_render(data.get('variables')) return tpl.async_render(data.get('variables'))
except (ValueError, TemplateError) as ex: except (ValueError, TemplateError) as ex:
return self.json_message('Error rendering template: {}'.format(ex), return self.json_message(
HTTP_BAD_REQUEST) "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST)
class APIErrorLog(HomeAssistantView): class APIErrorLog(HomeAssistantView):
"""View to fetch the error log.""" """View to fetch the API error log."""
url = URL_API_ERROR_LOG url = URL_API_ERROR_LOG
name = "api:error_log" name = 'api:error_log'
async def get(self, request): async def get(self, request):
"""Retrieve API error log.""" """Retrieve API error log."""
return web.FileResponse( return web.FileResponse(request.app['hass'].data[DATA_LOGGING])
request.app['hass'].data[DATA_LOGGING])
async def async_services_json(hass): async def async_services_json(hass):
"""Generate services data to JSONify.""" """Generate services data to JSONify."""
descriptions = await async_get_all_descriptions(hass) descriptions = await async_get_all_descriptions(hass)
return [{"domain": key, "services": value} return [{'domain': key, 'services': value}
for key, value in descriptions.items()] for key, value in descriptions.items()]
def async_events_json(hass): def async_events_json(hass):
"""Generate event data to JSONify.""" """Generate event data to JSONify."""
return [{"event": key, "listener_count": value} return [{'event': key, 'listener_count': value}
for key, value in hass.bus.async_listeners().items()] for key, value in hass.bus.async_listeners().items()]

View File

@ -17,7 +17,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyatv==0.3.9'] REQUIREMENTS = ['pyatv==0.3.10']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -98,7 +98,7 @@ SERVICE_SCHEMA = vol.Schema({
}) })
TRIGGER_SERVICE_SCHEMA = vol.Schema({ TRIGGER_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_VARIABLES, default={}): dict, vol.Optional(ATTR_VARIABLES, default={}): dict,
}) })

View File

@ -6,7 +6,8 @@ https://home-assistant.io/components/binary_sensor.deconz/
""" """
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz import ( from homeassistant.components.deconz import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID,
DATA_DECONZ_UNSUB)
from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -27,10 +28,13 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
"""Add binary sensor from deCONZ.""" """Add binary sensor from deCONZ."""
from pydeconz.sensor import DECONZ_BINARY_SENSOR from pydeconz.sensor import DECONZ_BINARY_SENSOR
entities = [] entities = []
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors: for sensor in sensors:
if sensor.type in DECONZ_BINARY_SENSOR: if sensor.type in DECONZ_BINARY_SENSOR and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
entities.append(DeconzBinarySensor(sensor)) entities.append(DeconzBinarySensor(sensor))
async_add_devices(entities, True) async_add_devices(entities, True)
hass.data[DATA_DECONZ_UNSUB].append( hass.data[DATA_DECONZ_UNSUB].append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
@ -103,6 +107,6 @@ class DeconzBinarySensor(BinarySensorDevice):
attr = {} attr = {}
if self._sensor.battery: if self._sensor.battery:
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
if self._sensor.type in PRESENCE and self._sensor.dark: if self._sensor.type in PRESENCE and self._sensor.dark is not None:
attr['dark'] = self._sensor.dark attr['dark'] = self._sensor.dark
return attr return attr

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/binary_sensor.envisalink/
""" """
import asyncio import asyncio
import logging import logging
import datetime
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -14,6 +15,7 @@ from homeassistant.components.envisalink import (
DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice, DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice,
SIGNAL_ZONE_UPDATE) SIGNAL_ZONE_UPDATE)
from homeassistant.const import ATTR_LAST_TRIP_TIME from homeassistant.const import ATTR_LAST_TRIP_TIME
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -63,7 +65,25 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
attr = {} attr = {}
attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault']
# The Envisalink library returns a "last_fault" value that's the
# number of seconds since the last fault, up to a maximum of 327680
# seconds (65536 5-second ticks).
#
# We don't want the HA event log to fill up with a bunch of no-op
# "state changes" that are just that number ticking up once per poll
# interval, so we subtract it from the current second-accurate time
# unless it is already at the maximum value, in which case we set it
# to None since we can't determine the actual value.
seconds_ago = self._info['last_fault']
if seconds_ago < 65536 * 5:
now = dt_util.now().replace(microsecond=0)
delta = datetime.timedelta(seconds=seconds_ago)
last_trip_time = (now - delta).isoformat()
else:
last_trip_time = None
attr[ATTR_LAST_TRIP_TIME] = last_trip_time
return attr return attr
@property @property

View File

@ -0,0 +1,81 @@
"""
Support for Hydrawise sprinkler.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.hydrawise/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.hydrawise import (
BINARY_SENSORS, DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP,
DEVICE_MAP_INDEX)
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import CONF_MONITORED_CONDITIONS
DEPENDENCIES = ['hydrawise']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MONITORED_CONDITIONS, default=BINARY_SENSORS):
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a sensor for a Hydrawise device."""
hydrawise = hass.data[DATA_HYDRAWISE].data
sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
if sensor_type in ['status', 'rain_sensor']:
sensors.append(
HydrawiseBinarySensor(
hydrawise.controller_status, sensor_type))
else:
# create a sensor for each zone
for zone in hydrawise.relays:
zone_data = zone
zone_data['running'] = \
hydrawise.controller_status.get('running', False)
sensors.append(HydrawiseBinarySensor(zone_data, sensor_type))
add_devices(sensors, True)
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorDevice):
"""A sensor implementation for Hydrawise device."""
@property
def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
def update(self):
"""Get the latest data and updates the state."""
_LOGGER.debug("Updating Hydrawise binary sensor: %s", self._name)
mydata = self.hass.data[DATA_HYDRAWISE].data
if self._sensor_type == 'status':
self._state = mydata.status == 'All good!'
elif self._sensor_type == 'rain_sensor':
for sensor in mydata.sensors:
if sensor['name'] == 'Rain':
self._state = sensor['active'] == 1
elif self._sensor_type == 'is_watering':
if not mydata.running:
self._state = False
elif int(mydata.running[0]['relay']) == self.data['relay']:
self._state = True
else:
self._state = False
@property
def device_class(self):
"""Return the device class of the sensor type."""
return DEVICE_MAP[self._sensor_type][
DEVICE_MAP_INDEX.index('DEVICE_CLASS_INDEX')]

View File

@ -7,27 +7,36 @@ https://home-assistant.io/components/binary_sensor.nest/
from itertools import chain from itertools import chain
import logging import logging
from homeassistant.components.binary_sensor import (BinarySensorDevice) from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.sensor.nest import NestSensor from homeassistant.components.nest import DATA_NEST, NestSensorDevice
from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.components.nest import DATA_NEST
DEPENDENCIES = ['nest'] DEPENDENCIES = ['nest']
BINARY_TYPES = ['online'] BINARY_TYPES = {'online': 'connectivity'}
CLIMATE_BINARY_TYPES = [ CLIMATE_BINARY_TYPES = {
'fan', 'fan': None,
'is_using_emergency_heat', 'is_using_emergency_heat': 'heat',
'is_locked', 'is_locked': None,
'has_leaf', 'has_leaf': None,
] }
CAMERA_BINARY_TYPES = [ CAMERA_BINARY_TYPES = {
'motion_detected', 'motion_detected': 'motion',
'sound_detected', 'sound_detected': 'sound',
'person_detected', 'person_detected': 'occupancy',
] }
STRUCTURE_BINARY_TYPES = {
'away': None,
# 'security_state', # pending python-nest update
}
STRUCTURE_BINARY_STATE_MAP = {
'away': {'away': True, 'home': False},
'security_state': {'deter': True, 'ok': False},
}
_BINARY_TYPES_DEPRECATED = [ _BINARY_TYPES_DEPRECATED = [
'hvac_ac_state', 'hvac_ac_state',
@ -40,8 +49,8 @@ _BINARY_TYPES_DEPRECATED = [
'hvac_emer_heat_state', 'hvac_emer_heat_state',
] ]
_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \ _VALID_BINARY_SENSOR_TYPES = {**BINARY_TYPES, **CLIMATE_BINARY_TYPES,
+ CAMERA_BINARY_TYPES **CAMERA_BINARY_TYPES, **STRUCTURE_BINARY_TYPES}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -68,6 +77,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error(wstr) _LOGGER.error(wstr)
sensors = [] sensors = []
for structure in nest.structures():
sensors += [NestBinarySensor(structure, None, variable)
for variable in conditions
if variable in STRUCTURE_BINARY_TYPES]
device_chain = chain(nest.thermostats(), device_chain = chain(nest.thermostats(),
nest.smoke_co_alarms(), nest.smoke_co_alarms(),
nest.cameras()) nest.cameras())
@ -88,11 +101,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensors += [NestActivityZoneSensor(structure, sensors += [NestActivityZoneSensor(structure,
device, device,
activity_zone)] activity_zone)]
add_devices(sensors, True) add_devices(sensors, True)
class NestBinarySensor(NestSensor, BinarySensorDevice): class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
"""Represents a Nest binary sensor.""" """Represents a Nest binary sensor."""
@property @property
@ -100,9 +112,19 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
return self._state return self._state
@property
def device_class(self):
"""Return the device class of the binary sensor."""
return _VALID_BINARY_SENSOR_TYPES.get(self.variable)
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
self._state = bool(getattr(self.device, self.variable)) value = getattr(self.device, self.variable)
if self.variable in STRUCTURE_BINARY_TYPES:
self._state = bool(STRUCTURE_BINARY_STATE_MAP
[self.variable][value])
else:
self._state = bool(value)
class NestActivityZoneSensor(NestBinarySensor): class NestActivityZoneSensor(NestBinarySensor):
@ -115,9 +137,9 @@ class NestActivityZoneSensor(NestBinarySensor):
self._name = "{} {} activity".format(self._name, self.zone.name) self._name = "{} {} activity".format(self._name, self.zone.name)
@property @property
def name(self): def device_class(self):
"""Return the name of the nest, if any.""" """Return the device class of the binary sensor."""
return self._name return 'motion'
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""

View File

@ -0,0 +1,102 @@
"""
This platform provides binary sensors for key RainMachine data.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.rainmachine/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.rainmachine import (
BINARY_SENSORS, DATA_RAINMACHINE, DATA_UPDATE_TOPIC, TYPE_FREEZE,
TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH,
TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity)
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['rainmachine']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the RainMachine Switch platform."""
if discovery_info is None:
return
rainmachine = hass.data[DATA_RAINMACHINE]
binary_sensors = []
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
name, icon = BINARY_SENSORS[sensor_type]
binary_sensors.append(
RainMachineBinarySensor(rainmachine, sensor_type, name, icon))
add_devices(binary_sensors, True)
class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
"""A sensor implementation for raincloud device."""
def __init__(self, rainmachine, sensor_type, name, icon):
"""Initialize the sensor."""
super().__init__(rainmachine)
self._icon = icon
self._name = name
self._sensor_type = sensor_type
self._state = None
@property
def icon(self) -> str:
"""Return the icon."""
return self._icon
@property
def is_on(self):
"""Return the status of the sensor."""
return self._state
@property
def should_poll(self):
"""Disable polling."""
return False
@property
def unique_id(self) -> str:
"""Return a unique, HASS-friendly identifier for this entity."""
return '{0}_{1}'.format(
self.rainmachine.device_mac.replace(':', ''), self._sensor_type)
@callback
def update_data(self):
"""Update the state."""
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC,
self.update_data)
def update(self):
"""Update the state."""
if self._sensor_type == TYPE_FREEZE:
self._state = self.rainmachine.restrictions['current']['freeze']
elif self._sensor_type == TYPE_FREEZE_PROTECTION:
self._state = self.rainmachine.restrictions['global'][
'freezeProtectEnabled']
elif self._sensor_type == TYPE_HOT_DAYS:
self._state = self.rainmachine.restrictions['global'][
'hotDaysExtraWatering']
elif self._sensor_type == TYPE_HOURLY:
self._state = self.rainmachine.restrictions['current']['hourly']
elif self._sensor_type == TYPE_MONTH:
self._state = self.rainmachine.restrictions['current']['month']
elif self._sensor_type == TYPE_RAINDELAY:
self._state = self.rainmachine.restrictions['current']['rainDelay']
elif self._sensor_type == TYPE_RAINSENSOR:
self._state = self.rainmachine.restrictions['current'][
'rainSensor']
elif self._sensor_type == TYPE_WEEKDAY:
self._state = self.rainmachine.restrictions['current']['weekDay']

View File

@ -4,7 +4,6 @@ Support for showing random states.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.random/ https://home-assistant.io/components/binary_sensor.random/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -24,8 +23,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine async def async_setup_platform(
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): hass, config, async_add_devices, discovery_info=None):
"""Set up the Random binary sensor.""" """Set up the Random binary sensor."""
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
device_class = config.get(CONF_DEVICE_CLASS) device_class = config.get(CONF_DEVICE_CLASS)
@ -57,8 +56,7 @@ class RandomSensor(BinarySensorDevice):
"""Return the sensor class of the sensor.""" """Return the sensor class of the sensor."""
return self._device_class return self._device_class
@asyncio.coroutine async def async_update(self):
def async_update(self):
"""Get new state and update the sensor's state.""" """Get new state and update the sensor's state."""
from random import getrandbits from random import getrandbits
self._state = bool(getrandbits(1)) self._state = bool(getrandbits(1))

View File

@ -330,6 +330,8 @@ class XiaomiButton(XiaomiBinarySensor):
click_type = 'both' click_type = 'both'
elif value == 'shake': elif value == 'shake':
click_type = 'shake' click_type = 'shake'
elif value == 'long_click':
return False
else: else:
_LOGGER.warning("Unsupported click_type detected: %s", value) _LOGGER.warning("Unsupported click_type detected: %s", value)
return False return False

View File

@ -203,14 +203,19 @@ class Switch(zha.Entity, BinarySensorDevice):
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Initialize Switch.""" """Initialize Switch."""
super().__init__(**kwargs) super().__init__(**kwargs)
self._state = True self._state = False
self._level = 255 self._level = 0
from zigpy.zcl.clusters import general from zigpy.zcl.clusters import general
self._out_listeners = { self._out_listeners = {
general.OnOff.cluster_id: self.OnOffListener(self), general.OnOff.cluster_id: self.OnOffListener(self),
general.LevelControl.cluster_id: self.LevelListener(self), general.LevelControl.cluster_id: self.LevelListener(self),
} }
@property
def should_poll(self) -> bool:
"""Let zha handle polling."""
return False
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""

View File

@ -22,6 +22,12 @@ from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF,
STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE,
PRECISION_TENTHS, ) PRECISION_TENTHS, )
DEFAULT_MIN_TEMP = 7
DEFAULT_MAX_TEMP = 35
DEFAULT_MIN_HUMITIDY = 30
DEFAULT_MAX_HUMIDITY = 99
DOMAIN = 'climate' DOMAIN = 'climate'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -778,19 +784,21 @@ class ClimateDevice(Entity):
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return convert_temperature(7, TEMP_CELSIUS, self.temperature_unit) return convert_temperature(DEFAULT_MIN_TEMP, TEMP_CELSIUS,
self.temperature_unit)
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return convert_temperature(35, TEMP_CELSIUS, self.temperature_unit) return convert_temperature(DEFAULT_MAX_TEMP, TEMP_CELSIUS,
self.temperature_unit)
@property @property
def min_humidity(self): def min_humidity(self):
"""Return the minimum humidity.""" """Return the minimum humidity."""
return 30 return DEFAULT_MIN_HUMITIDY
@property @property
def max_humidity(self): def max_humidity(self):
"""Return the maximum humidity.""" """Return the maximum humidity."""
return 99 return DEFAULT_MAX_HUMIDITY

View File

@ -14,7 +14,8 @@ from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice,
ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE, ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE,
SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA,
DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF,
@ -267,8 +268,7 @@ class GenericThermostat(ClimateDevice):
if self._min_temp: if self._min_temp:
return self._min_temp return self._min_temp
# get default temp from super class return DEFAULT_MIN_TEMP
return ClimateDevice.min_temp.fget(self)
@property @property
def max_temp(self): def max_temp(self):
@ -277,8 +277,7 @@ class GenericThermostat(ClimateDevice):
if self._max_temp: if self._max_temp:
return self._max_temp return self._max_temp
# Get default temp from super class return DEFAULT_MAX_TEMP
return ClimateDevice.max_temp.fget(self)
@asyncio.coroutine @asyncio.coroutine
def _async_sensor_changed(self, entity_id, old_state, new_state): def _async_sensor_changed(self, entity_id, old_state, new_state):

View File

@ -0,0 +1,101 @@
"""
Support for HomematicIP climate.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/climate.homematicip_cloud/
"""
import logging
from homeassistant.components.climate import (
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, ATTR_TEMPERATURE,
STATE_AUTO, STATE_MANUAL)
from homeassistant.const import TEMP_CELSIUS
from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
ATTR_HOME_ID)
_LOGGER = logging.getLogger(__name__)
STATE_BOOST = 'Boost'
HA_STATE_TO_HMIP = {
STATE_AUTO: 'AUTOMATIC',
STATE_MANUAL: 'MANUAL',
}
HMIP_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_HMIP.items()}
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the HomematicIP climate devices."""
from homematicip.group import HeatingGroup
if discovery_info is None:
return
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
devices = []
for device in home.groups:
if isinstance(device, HeatingGroup):
devices.append(HomematicipHeatingGroup(home, device))
if devices:
async_add_devices(devices)
class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice):
"""Representation of a MomematicIP heating group."""
def __init__(self, home, device):
"""Initialize heating group."""
device.modelType = 'Group-Heating'
super().__init__(home, device)
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_TARGET_TEMPERATURE
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._device.setPointTemperature
@property
def current_temperature(self):
"""Return the current temperature."""
return self._device.actualTemperature
@property
def current_humidity(self):
"""Return the current humidity."""
return self._device.humidity
@property
def current_operation(self):
"""Return current operation ie. automatic or manual."""
return HMIP_STATE_TO_HA.get(self._device.controlMode)
@property
def min_temp(self):
"""Return the minimum temperature."""
return self._device.minTemperature
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._device.maxTemperature
async def async_set_temperature(self, **kwargs):
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
await self._device.set_point_temperature(temperature)

View File

@ -8,7 +8,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.nest import DATA_NEST from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice,
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
@ -18,6 +18,7 @@ from homeassistant.components.climate import (
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN) CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['nest'] DEPENDENCIES = ['nest']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,11 +38,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
temp_unit = hass.config.units.temperature_unit temp_unit = hass.config.units.temperature_unit
add_devices( all_devices = [NestThermostat(structure, device, temp_unit)
[NestThermostat(structure, device, temp_unit) for structure, device in hass.data[DATA_NEST].thermostats()]
for structure, device in hass.data[DATA_NEST].thermostats()],
True add_devices(all_devices, True)
)
class NestThermostat(ClimateDevice): class NestThermostat(ClimateDevice):
@ -97,6 +97,20 @@ class NestThermostat(ClimateDevice):
self._min_temperature = None self._min_temperature = None
self._max_temperature = None self._max_temperature = None
@property
def should_poll(self):
"""Do not need poll thanks using Nest streaming API."""
return False
async def async_added_to_hass(self):
"""Register update signal handler."""
async def async_update_state():
"""Update device state."""
await self.async_update_ha_state(True)
async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE,
async_update_state)
@property @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
@ -170,18 +184,24 @@ class NestThermostat(ClimateDevice):
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
import nest import nest
temp = None
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if self._mode == NEST_MODE_HEAT_COOL: if self._mode == NEST_MODE_HEAT_COOL:
if target_temp_low is not None and target_temp_high is not None: if target_temp_low is not None and target_temp_high is not None:
temp = (target_temp_low, target_temp_high) temp = (target_temp_low, target_temp_high)
_LOGGER.debug("Nest set_temperature-output-value=%s", temp)
else: else:
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
_LOGGER.debug("Nest set_temperature-output-value=%s", temp) _LOGGER.debug("Nest set_temperature-output-value=%s", temp)
try: try:
if temp is not None:
self.device.target = temp self.device.target = temp
except nest.nest.APIError: except nest.nest.APIError as api_error:
_LOGGER.error("An error occurred while setting the temperature") _LOGGER.error("An error occurred while setting temperature: %s",
api_error)
# restore target temperature
self.schedule_update_ha_state(True)
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set operation mode.""" """Set operation mode."""

View File

@ -19,7 +19,7 @@ from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA, ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE,
SUPPORT_ON_OFF) SUPPORT_ON_OFF, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -154,7 +154,8 @@ class SensiboClimate(ClimateDevice):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return {ATTR_CURRENT_HUMIDITY: self.current_humidity} return {ATTR_CURRENT_HUMIDITY: self.current_humidity,
'battery': self.current_battery}
@property @property
def temperature_unit(self): def temperature_unit(self):
@ -191,6 +192,11 @@ class SensiboClimate(ClimateDevice):
"""Return the current humidity.""" """Return the current humidity."""
return self._measurements['humidity'] return self._measurements['humidity']
@property
def current_battery(self):
"""Return the current battery voltage."""
return self._measurements.get('batteryVoltage')
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
@ -240,13 +246,13 @@ class SensiboClimate(ClimateDevice):
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return self._temperatures_list[0] \ return self._temperatures_list[0] \
if self._temperatures_list else super().min_temp if self._temperatures_list else DEFAULT_MIN_TEMP
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return self._temperatures_list[-1] \ return self._temperatures_list[-1] \
if self._temperatures_list else super().max_temp if self._temperatures_list else DEFAULT_MAX_TEMP
@property @property
def unique_id(self): def unique_id(self):

View File

@ -8,7 +8,8 @@ import logging
from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS) from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS)
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
from homeassistant.const import ATTR_TEMPERATURE from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.components.tado import DATA_TADO from homeassistant.components.tado import DATA_TADO
@ -232,16 +233,16 @@ class TadoClimate(ClimateDevice):
"""Return the minimum temperature.""" """Return the minimum temperature."""
if self._min_temp: if self._min_temp:
return self._min_temp return self._min_temp
# get default temp from super class
return super().min_temp return DEFAULT_MIN_TEMP
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
if self._max_temp: if self._max_temp:
return self._max_temp return self._max_temp
# Get default temp from super class
return super().max_temp return DEFAULT_MAX_TEMP
def update(self): def update(self):
"""Update the state of this climate device.""" """Update the state of this climate device."""

View File

@ -185,7 +185,7 @@ class CloudIoT:
yield from client.send_json(response) yield from client.send_json(response)
except client_exceptions.WSServerHandshakeError as err: except client_exceptions.WSServerHandshakeError as err:
if err.code == 401: if err.status == 401:
disconnect_warn = 'Invalid auth.' disconnect_warn = 'Invalid auth.'
self.close_requested = True self.close_requested = True
# Should we notify user? # Should we notify user?

View File

@ -21,7 +21,7 @@ ON_DEMAND = ('zwave',)
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up the config component.""" """Set up the config component."""
await hass.components.frontend.async_register_built_in_panel( await hass.components.frontend.async_register_built_in_panel(
'config', 'config', 'mdi:settings') 'config', 'config', 'hass:settings')
async def setup_panel(panel_name): async def setup_panel(panel_name):
"""Set up a panel.""" """Set up a panel."""

View File

@ -9,9 +9,9 @@ import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME
from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME)
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.restore_state import async_get_last_state
@ -94,9 +94,8 @@ def async_reset(hass, entity_id):
DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id})) DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id}))
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config): """Set up the counters."""
"""Set up a counter."""
component = EntityComponent(_LOGGER, DOMAIN, hass) component = EntityComponent(_LOGGER, DOMAIN, hass)
entities = [] entities = []
@ -115,8 +114,7 @@ def async_setup(hass, config):
if not entities: if not entities:
return False return False
@asyncio.coroutine async def async_handler_service(service):
def async_handler_service(service):
"""Handle a call to the counter services.""" """Handle a call to the counter services."""
target_counters = component.async_extract_from_service(service) target_counters = component.async_extract_from_service(service)
@ -129,7 +127,7 @@ def async_setup(hass, config):
tasks = [getattr(counter, attr)() for counter in target_counters] tasks = [getattr(counter, attr)() for counter in target_counters]
if tasks: if tasks:
yield from asyncio.wait(tasks, loop=hass.loop) await asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_INCREMENT, async_handler_service) DOMAIN, SERVICE_INCREMENT, async_handler_service)
@ -138,7 +136,7 @@ def async_setup(hass, config):
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_RESET, async_handler_service) DOMAIN, SERVICE_RESET, async_handler_service)
yield from component.async_add_entities(entities) await component.async_add_entities(entities)
return True return True
@ -181,30 +179,26 @@ class Counter(Entity):
ATTR_STEP: self._step, ATTR_STEP: self._step,
} }
@asyncio.coroutine async def async_added_to_hass(self):
def async_added_to_hass(self):
"""Call when entity about to be added to Home Assistant.""" """Call when entity about to be added to Home Assistant."""
# If not None, we got an initial value. # If not None, we got an initial value.
if self._state is not None: if self._state is not None:
return return
state = yield from async_get_last_state(self.hass, self.entity_id) state = await async_get_last_state(self.hass, self.entity_id)
self._state = state and state.state == state self._state = state and state.state == state
@asyncio.coroutine async def async_decrement(self):
def async_decrement(self):
"""Decrement the counter.""" """Decrement the counter."""
self._state -= self._step self._state -= self._step
yield from self.async_update_ha_state() await self.async_update_ha_state()
@asyncio.coroutine async def async_increment(self):
def async_increment(self):
"""Increment a counter.""" """Increment a counter."""
self._state += self._step self._state += self._step
yield from self.async_update_ha_state() await self.async_update_ha_state()
@asyncio.coroutine async def async_reset(self):
def async_reset(self):
"""Reset a counter.""" """Reset a counter."""
self._state = self._initial self._state = self._initial
yield from self.async_update_ha_state() await self.async_update_ha_state()

View File

@ -69,6 +69,11 @@ class MyQDevice(CoverDevice):
self._name = device['name'] self._name = device['name']
self._status = STATE_CLOSED self._status = STATE_CLOSED
@property
def device_class(self):
"""Define this cover as a garage door."""
return 'garage'
@property @property
def should_poll(self): def should_poll(self):
"""Poll for state.""" """Poll for state."""

View File

@ -0,0 +1,103 @@
"""
Ryobi platform for the cover component.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.ryobi_gdo/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.cover import (
CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, STATE_UNKNOWN, STATE_CLOSED)
REQUIREMENTS = ['py_ryobi_gdo==0.0.10']
_LOGGER = logging.getLogger(__name__)
CONF_DEVICE_ID = 'device_id'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
})
SUPPORTED_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Ryobi covers."""
from py_ryobi_gdo import RyobiGDO as ryobi_door
covers = []
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
devices = config.get(CONF_DEVICE_ID)
for device_id in devices:
my_door = ryobi_door(username, password, device_id)
_LOGGER.debug("Getting the API key")
if my_door.get_api_key() is False:
_LOGGER.error("Wrong credentials, no API key retrieved")
return
_LOGGER.debug("Checking if the device ID is present")
if my_door.check_device_id() is False:
_LOGGER.error("%s not in your device list", device_id)
return
_LOGGER.debug("Adding device %s to covers", device_id)
covers.append(RyobiCover(hass, my_door))
if covers:
_LOGGER.debug("Adding covers")
add_devices(covers, True)
class RyobiCover(CoverDevice):
"""Representation of a ryobi cover."""
def __init__(self, hass, ryobi_door):
"""Initialize the cover."""
self.ryobi_door = ryobi_door
self._name = 'ryobi_gdo_{}'.format(ryobi_door.get_device_id())
self._door_state = None
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
if self._door_state == STATE_UNKNOWN:
return False
return self._door_state == STATE_CLOSED
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'garage'
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORTED_FEATURES
def close_cover(self, **kwargs):
"""Close the cover."""
_LOGGER.debug("Closing garage door")
self.ryobi_door.close_device()
def open_cover(self, **kwargs):
"""Open the cover."""
_LOGGER.debug("Opening garage door")
self.ryobi_door.open_device()
def update(self):
"""Update status from the door."""
_LOGGER.debug("Updating RyobiGDO status")
self.ryobi_door.update()
self._door_state = self.ryobi_door.get_door_status()

View File

@ -19,8 +19,14 @@
"link": { "link": {
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button", "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button",
"title": "Link with deCONZ" "title": "Link with deCONZ"
},
"options": {
"title": "Extra configuration options for deCONZ",
"data": {
"allow_clip_sensor": "Allow importing virtual sensors"
}
} }
}, },
"title": "deCONZ" "title": "deCONZ Zigbee gateway"
} }
} }

View File

@ -19,8 +19,8 @@ from homeassistant.util.json import load_json
# Loading the config flow file will register the flow # Loading the config flow file will register the flow
from .config_flow import configured_hosts from .config_flow import configured_hosts
from .const import ( from .const import (
CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT,
DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
REQUIREMENTS = ['pydeconz==38'] REQUIREMENTS = ['pydeconz==38']
@ -104,8 +104,10 @@ async def async_setup_entry(hass, config_entry):
def async_add_remote(sensors): def async_add_remote(sensors):
"""Setup remote from deCONZ.""" """Setup remote from deCONZ."""
from pydeconz.sensor import SWITCH as DECONZ_REMOTE from pydeconz.sensor import SWITCH as DECONZ_REMOTE
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors: for sensor in sensors:
if sensor.type in DECONZ_REMOTE: if sensor.type in DECONZ_REMOTE and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor)) hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor))
hass.data[DATA_DECONZ_UNSUB].append( hass.data[DATA_DECONZ_UNSUB].append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote)) async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote))

View File

@ -8,13 +8,15 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.util.json import load_json from homeassistant.util.json import load_json
from .const import CONFIG_FILE, DOMAIN from .const import CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN
CONF_BRIDGEID = 'bridgeid'
@callback @callback
def configured_hosts(hass): def configured_hosts(hass):
"""Return a set of the configured hosts.""" """Return a set of the configured hosts."""
return set(entry.data['host'] for entry return set(entry.data[CONF_HOST] for entry
in hass.config_entries.async_entries(DOMAIN)) in hass.config_entries.async_entries(DOMAIN))
@ -30,7 +32,12 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
self.deconz_config = {} self.deconz_config = {}
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None):
"""Handle a deCONZ config flow start.""" """Handle a deCONZ config flow start.
Only allows one instance to be set up.
If only one bridge is found go to link step.
If more than one bridge is found let user choose bridge to link.
"""
from pydeconz.utils import async_discovery from pydeconz.utils import async_discovery
if configured_hosts(self.hass): if configured_hosts(self.hass):
@ -65,7 +72,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
async def async_step_link(self, user_input=None): async def async_step_link(self, user_input=None):
"""Attempt to link with the deCONZ bridge.""" """Attempt to link with the deCONZ bridge."""
from pydeconz.utils import async_get_api_key, async_get_bridgeid from pydeconz.utils import async_get_api_key
errors = {} errors = {}
if user_input is not None: if user_input is not None:
@ -75,13 +82,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
api_key = await async_get_api_key(session, **self.deconz_config) api_key = await async_get_api_key(session, **self.deconz_config)
if api_key: if api_key:
self.deconz_config[CONF_API_KEY] = api_key self.deconz_config[CONF_API_KEY] = api_key
if 'bridgeid' not in self.deconz_config: return await self.async_step_options()
self.deconz_config['bridgeid'] = await async_get_bridgeid(
session, **self.deconz_config)
return self.async_create_entry(
title='deCONZ-' + self.deconz_config['bridgeid'],
data=self.deconz_config
)
errors['base'] = 'no_key' errors['base'] = 'no_key'
return self.async_show_form( return self.async_show_form(
@ -89,6 +90,34 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
errors=errors, errors=errors,
) )
async def async_step_options(self, user_input=None):
"""Extra options for deCONZ.
CONF_CLIP_SENSOR -- Allow user to choose if they want clip sensors.
"""
from pydeconz.utils import async_get_bridgeid
if user_input is not None:
self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = \
user_input[CONF_ALLOW_CLIP_SENSOR]
if CONF_BRIDGEID not in self.deconz_config:
session = aiohttp_client.async_get_clientsession(self.hass)
self.deconz_config[CONF_BRIDGEID] = await async_get_bridgeid(
session, **self.deconz_config)
return self.async_create_entry(
title='deCONZ-' + self.deconz_config[CONF_BRIDGEID],
data=self.deconz_config
)
return self.async_show_form(
step_id='options',
data_schema=vol.Schema({
vol.Optional(CONF_ALLOW_CLIP_SENSOR): bool,
}),
)
async def async_step_discovery(self, discovery_info): async def async_step_discovery(self, discovery_info):
"""Prepare configuration for a discovered deCONZ bridge. """Prepare configuration for a discovered deCONZ bridge.
@ -97,7 +126,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
deconz_config = {} deconz_config = {}
deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST) deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
deconz_config['bridgeid'] = discovery_info.get('serial') deconz_config[CONF_BRIDGEID] = discovery_info.get('serial')
config_file = await self.hass.async_add_job( config_file = await self.hass.async_add_job(
load_json, self.hass.config.path(CONFIG_FILE)) load_json, self.hass.config.path(CONFIG_FILE))
@ -121,19 +150,15 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler):
Otherwise we will delegate to `link` step which Otherwise we will delegate to `link` step which
will ask user to link the bridge. will ask user to link the bridge.
""" """
from pydeconz.utils import async_get_bridgeid
if configured_hosts(self.hass): if configured_hosts(self.hass):
return self.async_abort(reason='one_instance_only') return self.async_abort(reason='one_instance_only')
elif CONF_API_KEY not in import_config:
self.deconz_config = import_config self.deconz_config = import_config
if CONF_API_KEY not in import_config:
return await self.async_step_link() return await self.async_step_link()
if 'bridgeid' not in import_config: self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True
session = aiohttp_client.async_get_clientsession(self.hass)
import_config['bridgeid'] = await async_get_bridgeid(
session, **import_config)
return self.async_create_entry( return self.async_create_entry(
title='deCONZ-' + import_config['bridgeid'], title='deCONZ-' + self.deconz_config[CONF_BRIDGEID],
data=import_config data=self.deconz_config
) )

View File

@ -8,3 +8,5 @@ CONFIG_FILE = 'deconz.conf'
DATA_DECONZ_EVENT = 'deconz_events' DATA_DECONZ_EVENT = 'deconz_events'
DATA_DECONZ_ID = 'deconz_entities' DATA_DECONZ_ID = 'deconz_entities'
DATA_DECONZ_UNSUB = 'deconz_dispatchers' DATA_DECONZ_UNSUB = 'deconz_dispatchers'
CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor'

View File

@ -1,6 +1,6 @@
{ {
"config": { "config": {
"title": "deCONZ", "title": "deCONZ Zigbee gateway",
"step": { "step": {
"init": { "init": {
"title": "Define deCONZ gateway", "title": "Define deCONZ gateway",
@ -12,6 +12,12 @@
"link": { "link": {
"title": "Link with deCONZ", "title": "Link with deCONZ",
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button"
},
"options": {
"title": "Extra configuration options for deCONZ",
"data":{
"allow_clip_sensor": "Allow importing virtual sensors"
}
} }
}, },
"error": { "error": {

View File

@ -4,19 +4,20 @@ Support for Google Maps location sharing.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.google_maps/ https://home-assistant.io/components/device_tracker.google_maps/
""" """
import logging
from datetime import timedelta from datetime import timedelta
import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, SOURCE_TYPE_GPS) PLATFORM_SCHEMA, SOURCE_TYPE_GPS)
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, ATTR_ID from homeassistant.const import ATTR_ID, CONF_PASSWORD, CONF_USERNAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
REQUIREMENTS = ['locationsharinglib==2.0.2'] REQUIREMENTS = ['locationsharinglib==2.0.7']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -70,7 +71,7 @@ class GoogleMapsScanner(object):
def _update_info(self, now=None): def _update_info(self, now=None):
for person in self.service.get_all_people(): for person in self.service.get_all_people():
try: try:
dev_id = 'google_maps_{0}'.format(person.id) dev_id = 'google_maps_{0}'.format(slugify(person.id))
except TypeError: except TypeError:
_LOGGER.warning("No location(s) shared with this account") _LOGGER.warning("No location(s) shared with this account")
return return

View File

@ -15,14 +15,18 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import (
CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_SSL)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_SSL = False
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean
}) })
@ -44,7 +48,9 @@ class LuciDeviceScanner(DeviceScanner):
def __init__(self, config): def __init__(self, config):
"""Initialize the scanner.""" """Initialize the scanner."""
self.host = config[CONF_HOST] host = config[CONF_HOST]
protocol = 'http' if not config[CONF_SSL] else 'https'
self.origin = '{}://{}'.format(protocol, host)
self.username = config[CONF_USERNAME] self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD] self.password = config[CONF_PASSWORD]
@ -57,7 +63,7 @@ class LuciDeviceScanner(DeviceScanner):
def refresh_token(self): def refresh_token(self):
"""Get a new token.""" """Get a new token."""
self.token = _get_token(self.host, self.username, self.password) self.token = _get_token(self.origin, self.username, self.password)
def scan_devices(self): def scan_devices(self):
"""Scan for new devices and return a list with found device IDs.""" """Scan for new devices and return a list with found device IDs."""
@ -67,9 +73,9 @@ class LuciDeviceScanner(DeviceScanner):
def get_device_name(self, device): def get_device_name(self, device):
"""Return the name of the given device or None if we don't know.""" """Return the name of the given device or None if we don't know."""
if self.mac2name is None: if self.mac2name is None:
url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host) url = '{}/cgi-bin/luci/rpc/uci'.format(self.origin)
result = _req_json_rpc(url, 'get_all', 'dhcp', result = _req_json_rpc(
params={'auth': self.token}) url, 'get_all', 'dhcp', params={'auth': self.token})
if result: if result:
hosts = [x for x in result.values() hosts = [x for x in result.values()
if x['.type'] == 'host' and if x['.type'] == 'host' and
@ -92,11 +98,11 @@ class LuciDeviceScanner(DeviceScanner):
_LOGGER.info("Checking ARP") _LOGGER.info("Checking ARP")
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host) url = '{}/cgi-bin/luci/rpc/sys'.format(self.origin)
try: try:
result = _req_json_rpc(url, 'net.arptable', result = _req_json_rpc(
params={'auth': self.token}) url, 'net.arptable', params={'auth': self.token})
except InvalidLuciTokenError: except InvalidLuciTokenError:
_LOGGER.info("Refreshing token") _LOGGER.info("Refreshing token")
self.refresh_token() self.refresh_token()
@ -146,10 +152,10 @@ def _req_json_rpc(url, method, *args, **kwargs):
raise InvalidLuciTokenError raise InvalidLuciTokenError
else: else:
_LOGGER.error('Invalid response from luci: %s', res) _LOGGER.error("Invalid response from luci: %s", res)
def _get_token(host, username, password): def _get_token(origin, username, password):
"""Get authentication token for the given host+username+password.""" """Get authentication token for the given configuration."""
url = 'http://{}/cgi-bin/luci/rpc/auth'.format(host) url = '{}/cgi-bin/luci/rpc/auth'.format(origin)
return _req_json_rpc(url, 'login', username, password) return _req_json_rpc(url, 'login', username, password)

View File

@ -83,6 +83,7 @@ SERVICE_HANDLERS = {
'songpal': ('media_player', 'songpal'), 'songpal': ('media_player', 'songpal'),
'kodi': ('media_player', 'kodi'), 'kodi': ('media_player', 'kodi'),
'volumio': ('media_player', 'volumio'), 'volumio': ('media_player', 'volumio'),
'nanoleaf_aurora': ('light', 'nanoleaf_aurora'),
} }
OPTIONAL_SERVICE_HANDLERS = { OPTIONAL_SERVICE_HANDLERS = {

View File

@ -15,7 +15,7 @@ from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['lakeside==0.6'] REQUIREMENTS = ['lakeside==0.7']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -25,7 +25,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20180531.0'] REQUIREMENTS = ['home-assistant-frontend==20180608.0b0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']

View File

@ -28,6 +28,15 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'hassio' DOMAIN = 'hassio'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
CONF_FRONTEND_REPO = 'development_repo'
CONFIG_SCHEMA = vol.Schema({
vol.Optional(DOMAIN): vol.Schema({
vol.Optional(CONF_FRONTEND_REPO): cv.isdir,
}),
}, extra=vol.ALLOW_EXTRA)
DATA_HOMEASSISTANT_VERSION = 'hassio_hass_version' DATA_HOMEASSISTANT_VERSION = 'hassio_hass_version'
HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) HASSIO_UPDATE_INTERVAL = timedelta(minutes=55)
@ -142,7 +151,13 @@ def async_setup(hass, config):
try: try:
host = os.environ['HASSIO'] host = os.environ['HASSIO']
except KeyError: except KeyError:
_LOGGER.error("No Hass.io supervisor detect") _LOGGER.error("Missing HASSIO environment variable.")
return False
try:
os.environ['HASSIO_TOKEN']
except KeyError:
_LOGGER.error("Missing HASSIO_TOKEN environment variable.")
return False return False
websession = hass.helpers.aiohttp_client.async_get_clientsession() websession = hass.helpers.aiohttp_client.async_get_clientsession()
@ -152,11 +167,18 @@ def async_setup(hass, config):
_LOGGER.error("Not connected with Hass.io") _LOGGER.error("Not connected with Hass.io")
return False return False
# This overrides the normal API call that would be forwarded
development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO)
if development_repo is not None:
hass.http.register_static_path(
'/api/hassio/app-es5',
os.path.join(development_repo, 'hassio/build-es5'), False)
hass.http.register_view(HassIOView(host, websession)) hass.http.register_view(HassIOView(host, websession))
if 'frontend' in hass.config.components: if 'frontend' in hass.config.components:
yield from hass.components.frontend.async_register_built_in_panel( yield from hass.components.frontend.async_register_built_in_panel(
'hassio', 'Hass.io', 'mdi:home-assistant') 'hassio', 'Hass.io', 'hass:home-assistant')
if 'http' in config: if 'http' in config:
yield from hassio.update_hass_api(config['http']) yield from hassio.update_hass_api(config['http'])

View File

@ -274,7 +274,7 @@ async def async_setup(hass, config):
hass.http.register_view(HistoryPeriodView(filters, use_include_order)) hass.http.register_view(HistoryPeriodView(filters, use_include_order))
await hass.components.frontend.async_register_built_in_panel( await hass.components.frontend.async_register_built_in_panel(
'history', 'history', 'mdi:poll-box') 'history', 'history', 'hass:poll-box')
return True return True

View File

@ -9,12 +9,11 @@ from zlib import adler32
import voluptuous as vol import voluptuous as vol
from homeassistant.components.cover import ( import homeassistant.components.cover as cover
SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION)
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, CONF_TYPE, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
TEMP_CELSIUS, TEMP_FAHRENHEIT) TEMP_CELSIUS, TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -22,15 +21,16 @@ from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.util import get_local_ip from homeassistant.util import get_local_ip
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from .const import ( from .const import (
CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER,
DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25,
SERVICE_HOMEKIT_START) DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, TYPE_OUTLET, TYPE_SWITCH)
from .util import show_setup_message, validate_entity_config from .util import (
show_setup_message, validate_entity_config, validate_media_player_features)
TYPES = Registry() TYPES = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['HAP-python==2.1.0'] REQUIREMENTS = ['HAP-python==2.2.2']
# #### Driver Status #### # #### Driver Status ####
STATUS_READY = 0 STATUS_READY = 0
@ -38,6 +38,8 @@ STATUS_RUNNING = 1
STATUS_STOPPED = 2 STATUS_STOPPED = 2
STATUS_WAIT = 3 STATUS_WAIT = 3
SWITCH_TYPES = {TYPE_OUTLET: 'Outlet',
TYPE_SWITCH: 'Switch'}
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All({ DOMAIN: vol.All({
@ -84,7 +86,7 @@ async def async_setup(hass, config):
return True return True
def get_accessory(hass, state, aid, config): def get_accessory(hass, driver, state, aid, config):
"""Take state and return an accessory object if supported.""" """Take state and return an accessory object if supported."""
if not aid: if not aid:
_LOGGER.warning('The entitiy "%s" is not supported, since it ' _LOGGER.warning('The entitiy "%s" is not supported, since it '
@ -109,11 +111,11 @@ def get_accessory(hass, state, aid, config):
device_class = state.attributes.get(ATTR_DEVICE_CLASS) device_class = state.attributes.get(ATTR_DEVICE_CLASS)
if device_class == 'garage' and \ if device_class == 'garage' and \
features & (SUPPORT_OPEN | SUPPORT_CLOSE): features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE):
a_type = 'GarageDoorOpener' a_type = 'GarageDoorOpener'
elif features & SUPPORT_SET_POSITION: elif features & cover.SUPPORT_SET_POSITION:
a_type = 'WindowCovering' a_type = 'WindowCovering'
elif features & (SUPPORT_OPEN | SUPPORT_CLOSE): elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE):
a_type = 'WindowCoveringBasic' a_type = 'WindowCoveringBasic'
elif state.domain == 'fan': elif state.domain == 'fan':
@ -125,6 +127,12 @@ def get_accessory(hass, state, aid, config):
elif state.domain == 'lock': elif state.domain == 'lock':
a_type = 'Lock' a_type = 'Lock'
elif state.domain == 'media_player':
feature_list = config.get(CONF_FEATURE_LIST)
if feature_list and \
validate_media_player_features(state, feature_list):
a_type = 'MediaPlayer'
elif state.domain == 'sensor': elif state.domain == 'sensor':
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
device_class = state.attributes.get(ATTR_DEVICE_CLASS) device_class = state.attributes.get(ATTR_DEVICE_CLASS)
@ -143,14 +151,18 @@ def get_accessory(hass, state, aid, config):
elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'): elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'):
a_type = 'LightSensor' a_type = 'LightSensor'
elif state.domain in ('switch', 'remote', 'input_boolean', 'script'): elif state.domain == 'switch':
switch_type = config.get(CONF_TYPE, TYPE_SWITCH)
a_type = SWITCH_TYPES[switch_type]
elif state.domain in ('automation', 'input_boolean', 'remote', 'script'):
a_type = 'Switch' a_type = 'Switch'
if a_type is None: if a_type is None:
return None return None
_LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type)
return TYPES[a_type](hass, name, state.entity_id, aid, config) return TYPES[a_type](hass, driver, name, state.entity_id, aid, config)
def generate_aid(entity_id): def generate_aid(entity_id):
@ -185,9 +197,9 @@ class HomeKit():
ip_addr = self._ip_address or get_local_ip() ip_addr = self._ip_address or get_local_ip()
path = self.hass.config.path(HOMEKIT_FILE) path = self.hass.config.path(HOMEKIT_FILE)
self.bridge = HomeBridge(self.hass) self.driver = HomeDriver(self.hass, address=ip_addr,
self.driver = HomeDriver(self.hass, self.bridge, port=self._port, port=self._port, persist_file=path)
address=ip_addr, persist_file=path) self.bridge = HomeBridge(self.hass, self.driver)
def add_bridge_accessory(self, state): def add_bridge_accessory(self, state):
"""Try adding accessory to bridge if configured beforehand.""" """Try adding accessory to bridge if configured beforehand."""
@ -195,7 +207,7 @@ class HomeKit():
return return
aid = generate_aid(state.entity_id) aid = generate_aid(state.entity_id)
conf = self._config.pop(state.entity_id, {}) conf = self._config.pop(state.entity_id, {})
acc = get_accessory(self.hass, state, aid, conf) acc = get_accessory(self.hass, self.driver, state, aid, conf)
if acc is not None: if acc is not None:
self.bridge.add_accessory(acc) self.bridge.add_accessory(acc)
@ -208,12 +220,12 @@ class HomeKit():
# pylint: disable=unused-variable # pylint: disable=unused-variable
from . import ( # noqa F401 from . import ( # noqa F401
type_covers, type_fans, type_lights, type_locks, type_covers, type_fans, type_lights, type_locks,
type_security_systems, type_sensors, type_switches, type_media_players, type_security_systems, type_sensors,
type_thermostats) type_switches, type_thermostats)
for state in self.hass.states.all(): for state in self.hass.states.all():
self.add_bridge_accessory(state) self.add_bridge_accessory(state)
self.bridge.set_driver(self.driver) self.driver.add_accessory(self.bridge)
if not self.driver.state.paired: if not self.driver.state.paired:
show_setup_message(self.hass, self.driver.state.pincode) show_setup_message(self.hass, self.driver.state.pincode)

View File

@ -1,6 +1,6 @@
"""Extend the basic Accessory and Bridge functions.""" """Extend the basic Accessory and Bridge functions."""
from datetime import timedelta from datetime import timedelta
from functools import wraps from functools import partial, wraps
from inspect import getmodule from inspect import getmodule
import logging import logging
@ -27,35 +27,25 @@ _LOGGER = logging.getLogger(__name__)
def debounce(func): def debounce(func):
"""Decorator function. Debounce callbacks form HomeKit.""" """Decorator function. Debounce callbacks form HomeKit."""
@ha_callback @ha_callback
def call_later_listener(*args): def call_later_listener(self, *args):
"""Callback listener called from call_later.""" """Callback listener called from call_later."""
# pylint: disable=unsubscriptable-object debounce_params = self.debounce.pop(func.__name__, None)
nonlocal lastargs, remove_listener if debounce_params:
hass = lastargs['hass'] self.hass.async_add_job(func, self, *debounce_params[1:])
hass.async_add_job(func, *lastargs['args'])
lastargs = remove_listener = None
@wraps(func) @wraps(func)
def wrapper(*args): def wrapper(self, *args):
"""Wrapper starts async timer. """Wrapper starts async timer."""
debounce_params = self.debounce.pop(func.__name__, None)
The accessory must have 'self.hass' and 'self.entity_id' as attributes. if debounce_params:
""" debounce_params[0]() # remove listener
# pylint: disable=not-callable
hass = args[0].hass
nonlocal lastargs, remove_listener
if remove_listener:
remove_listener()
lastargs = remove_listener = None
lastargs = {'hass': hass, 'args': [*args]}
remove_listener = track_point_in_utc_time( remove_listener = track_point_in_utc_time(
hass, call_later_listener, self.hass, partial(call_later_listener, self),
dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT)) dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT))
logger.debug('%s: Start %s timeout', args[0].entity_id, self.debounce[func.__name__] = (remove_listener, *args)
logger.debug('%s: Start %s timeout', self.entity_id,
func.__name__.replace('set_', '')) func.__name__.replace('set_', ''))
remove_listener = None
lastargs = None
name = getmodule(func).__name__ name = getmodule(func).__name__
logger = logging.getLogger(name) logger = logging.getLogger(name)
return wrapper return wrapper
@ -64,10 +54,10 @@ def debounce(func):
class HomeAccessory(Accessory): class HomeAccessory(Accessory):
"""Adapter class for Accessory.""" """Adapter class for Accessory."""
def __init__(self, hass, name, entity_id, aid, config, def __init__(self, hass, driver, name, entity_id, aid, config,
category=CATEGORY_OTHER): category=CATEGORY_OTHER):
"""Initialize a Accessory object.""" """Initialize a Accessory object."""
super().__init__(name, aid=aid) super().__init__(driver, name, aid=aid)
model = split_entity_id(entity_id)[0].replace("_", " ").title() model = split_entity_id(entity_id)[0].replace("_", " ").title()
self.set_info_service( self.set_info_service(
firmware_revision=__version__, manufacturer=MANUFACTURER, firmware_revision=__version__, manufacturer=MANUFACTURER,
@ -76,11 +66,15 @@ class HomeAccessory(Accessory):
self.config = config self.config = config
self.entity_id = entity_id self.entity_id = entity_id
self.hass = hass self.hass = hass
self.debounce = {}
def run(self): async def run(self):
"""Method called by accessory after driver is started.""" """Method called by accessory after driver is started.
Run inside the HAP-python event loop.
"""
state = self.hass.states.get(self.entity_id) state = self.hass.states.get(self.entity_id)
self.update_state_callback(new_state=state) self.hass.add_job(self.update_state_callback, None, None, state)
async_track_state_change( async_track_state_change(
self.hass, self.entity_id, self.update_state_callback) self.hass, self.entity_id, self.update_state_callback)
@ -104,9 +98,9 @@ class HomeAccessory(Accessory):
class HomeBridge(Bridge): class HomeBridge(Bridge):
"""Adapter class for Bridge.""" """Adapter class for Bridge."""
def __init__(self, hass, name=BRIDGE_NAME): def __init__(self, hass, driver, name=BRIDGE_NAME):
"""Initialize a Bridge object.""" """Initialize a Bridge object."""
super().__init__(name) super().__init__(driver, name)
self.set_info_service( self.set_info_service(
firmware_revision=__version__, manufacturer=MANUFACTURER, firmware_revision=__version__, manufacturer=MANUFACTURER,
model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER) model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER)
@ -120,17 +114,17 @@ class HomeBridge(Bridge):
class HomeDriver(AccessoryDriver): class HomeDriver(AccessoryDriver):
"""Adapter class for AccessoryDriver.""" """Adapter class for AccessoryDriver."""
def __init__(self, hass, *args, **kwargs): def __init__(self, hass, **kwargs):
"""Initialize a AccessoryDriver object.""" """Initialize a AccessoryDriver object."""
super().__init__(*args, **kwargs) super().__init__(**kwargs)
self.hass = hass self.hass = hass
def pair(self, client_uuid, client_public): def pair(self, client_uuid, client_public):
"""Override super function to dismiss setup message if paired.""" """Override super function to dismiss setup message if paired."""
value = super().pair(client_uuid, client_public) success = super().pair(client_uuid, client_public)
if value: if success:
dismiss_setup_message(self.hass) dismiss_setup_message(self.hass)
return value return success
def unpair(self, client_uuid): def unpair(self, client_uuid):
"""Override super function to show setup message if unpaired.""" """Override super function to show setup message if unpaired."""

View File

@ -8,12 +8,20 @@ HOMEKIT_NOTIFY_ID = 4663548
# #### Config #### # #### Config ####
CONF_AUTO_START = 'auto_start' CONF_AUTO_START = 'auto_start'
CONF_ENTITY_CONFIG = 'entity_config' CONF_ENTITY_CONFIG = 'entity_config'
CONF_FEATURE = 'feature'
CONF_FEATURE_LIST = 'feature_list'
CONF_FILTER = 'filter' CONF_FILTER = 'filter'
# #### Config Defaults #### # #### Config Defaults ####
DEFAULT_AUTO_START = True DEFAULT_AUTO_START = True
DEFAULT_PORT = 51827 DEFAULT_PORT = 51827
# #### Features ####
FEATURE_ON_OFF = 'on_off'
FEATURE_PLAY_PAUSE = 'play_pause'
FEATURE_PLAY_STOP = 'play_stop'
FEATURE_TOGGLE_MUTE = 'toggle_mute'
# #### HomeKit Component Services #### # #### HomeKit Component Services ####
SERVICE_HOMEKIT_START = 'start' SERVICE_HOMEKIT_START = 'start'
@ -23,6 +31,10 @@ BRIDGE_NAME = 'Home Assistant Bridge'
BRIDGE_SERIAL_NUMBER = 'homekit.bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge'
MANUFACTURER = 'Home Assistant' MANUFACTURER = 'Home Assistant'
# #### Switch Types ####
TYPE_OUTLET = 'outlet'
TYPE_SWITCH = 'switch'
# #### Services #### # #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_ACCESSORY_INFO = 'AccessoryInformation'
SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
@ -38,6 +50,7 @@ SERV_LIGHTBULB = 'Lightbulb'
SERV_LOCK = 'LockMechanism' SERV_LOCK = 'LockMechanism'
SERV_MOTION_SENSOR = 'MotionSensor' SERV_MOTION_SENSOR = 'MotionSensor'
SERV_OCCUPANCY_SENSOR = 'OccupancySensor' SERV_OCCUPANCY_SENSOR = 'OccupancySensor'
SERV_OUTLET = 'Outlet'
SERV_SECURITY_SYSTEM = 'SecuritySystem' SERV_SECURITY_SYSTEM = 'SecuritySystem'
SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SMOKE_SENSOR = 'SmokeSensor'
SERV_SWITCH = 'Switch' SERV_SWITCH = 'Switch'
@ -76,6 +89,7 @@ CHAR_MODEL = 'Model'
CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_MOTION_DETECTED = 'MotionDetected'
CHAR_NAME = 'Name' CHAR_NAME = 'Name'
CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
CHAR_OUTLET_IN_USE = 'OutletInUse'
CHAR_ON = 'On' CHAR_ON = 'On'
CHAR_POSITION_STATE = 'PositionState' CHAR_POSITION_STATE = 'PositionState'
CHAR_ROTATION_DIRECTION = 'RotationDirection' CHAR_ROTATION_DIRECTION = 'RotationDirection'

View File

@ -0,0 +1,142 @@
"""Class to hold all media player accessories."""
import logging
from pyhap.const import CATEGORY_SWITCH
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE,
STATE_OFF, STATE_PLAYING, STATE_UNKNOWN)
from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_MUTED, DOMAIN)
from . import TYPES
from .accessories import HomeAccessory
from .const import (
CHAR_NAME, CHAR_ON, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE,
FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, SERV_SWITCH)
_LOGGER = logging.getLogger(__name__)
MODE_FRIENDLY_NAME = {FEATURE_ON_OFF: 'Power',
FEATURE_PLAY_PAUSE: 'Play/Pause',
FEATURE_PLAY_STOP: 'Play/Stop',
FEATURE_TOGGLE_MUTE: 'Mute'}
@TYPES.register('MediaPlayer')
class MediaPlayer(HomeAccessory):
"""Generate a Media Player accessory."""
def __init__(self, *args):
"""Initialize a Switch accessory object."""
super().__init__(*args, category=CATEGORY_SWITCH)
self._flag = {FEATURE_ON_OFF: False, FEATURE_PLAY_PAUSE: False,
FEATURE_PLAY_STOP: False, FEATURE_TOGGLE_MUTE: False}
self.chars = {FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None,
FEATURE_PLAY_STOP: None, FEATURE_TOGGLE_MUTE: None}
feature_list = self.config[CONF_FEATURE_LIST]
if FEATURE_ON_OFF in feature_list:
name = self.generate_service_name(FEATURE_ON_OFF)
serv_on_off = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
serv_on_off.configure_char(CHAR_NAME, value=name)
self.chars[FEATURE_ON_OFF] = serv_on_off.configure_char(
CHAR_ON, value=False, setter_callback=self.set_on_off)
if FEATURE_PLAY_PAUSE in feature_list:
name = self.generate_service_name(FEATURE_PLAY_PAUSE)
serv_play_pause = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
serv_play_pause.configure_char(CHAR_NAME, value=name)
self.chars[FEATURE_PLAY_PAUSE] = serv_play_pause.configure_char(
CHAR_ON, value=False, setter_callback=self.set_play_pause)
if FEATURE_PLAY_STOP in feature_list:
name = self.generate_service_name(FEATURE_PLAY_STOP)
serv_play_stop = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
serv_play_stop.configure_char(CHAR_NAME, value=name)
self.chars[FEATURE_PLAY_STOP] = serv_play_stop.configure_char(
CHAR_ON, value=False, setter_callback=self.set_play_stop)
if FEATURE_TOGGLE_MUTE in feature_list:
name = self.generate_service_name(FEATURE_TOGGLE_MUTE)
serv_toggle_mute = self.add_preload_service(SERV_SWITCH, CHAR_NAME)
serv_toggle_mute.configure_char(CHAR_NAME, value=name)
self.chars[FEATURE_TOGGLE_MUTE] = serv_toggle_mute.configure_char(
CHAR_ON, value=False, setter_callback=self.set_toggle_mute)
def generate_service_name(self, mode):
"""Generate name for individual service."""
return '{} {}'.format(self.display_name, MODE_FRIENDLY_NAME[mode])
def set_on_off(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state for "on_off" to %s',
self.entity_id, value)
self._flag[FEATURE_ON_OFF] = True
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
params = {ATTR_ENTITY_ID: self.entity_id}
self.hass.services.call(DOMAIN, service, params)
def set_play_pause(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state for "play_pause" to %s',
self.entity_id, value)
self._flag[FEATURE_PLAY_PAUSE] = True
service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE
params = {ATTR_ENTITY_ID: self.entity_id}
self.hass.services.call(DOMAIN, service, params)
def set_play_stop(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state for "play_stop" to %s',
self.entity_id, value)
self._flag[FEATURE_PLAY_STOP] = True
service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP
params = {ATTR_ENTITY_ID: self.entity_id}
self.hass.services.call(DOMAIN, service, params)
def set_toggle_mute(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state for "toggle_mute" to %s',
self.entity_id, value)
self._flag[FEATURE_TOGGLE_MUTE] = True
params = {ATTR_ENTITY_ID: self.entity_id,
ATTR_MEDIA_VOLUME_MUTED: value}
self.hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, params)
def update_state(self, new_state):
"""Update switch state after state changed."""
current_state = new_state.state
if self.chars[FEATURE_ON_OFF]:
hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN, 'None')
if not self._flag[FEATURE_ON_OFF]:
_LOGGER.debug('%s: Set current state for "on_off" to %s',
self.entity_id, hk_state)
self.chars[FEATURE_ON_OFF].set_value(hk_state)
self._flag[FEATURE_ON_OFF] = False
if self.chars[FEATURE_PLAY_PAUSE]:
hk_state = current_state == STATE_PLAYING
if not self._flag[FEATURE_PLAY_PAUSE]:
_LOGGER.debug('%s: Set current state for "play_pause" to %s',
self.entity_id, hk_state)
self.chars[FEATURE_PLAY_PAUSE].set_value(hk_state)
self._flag[FEATURE_PLAY_PAUSE] = False
if self.chars[FEATURE_PLAY_STOP]:
hk_state = current_state == STATE_PLAYING
if not self._flag[FEATURE_PLAY_STOP]:
_LOGGER.debug('%s: Set current state for "play_stop" to %s',
self.entity_id, hk_state)
self.chars[FEATURE_PLAY_STOP].set_value(hk_state)
self._flag[FEATURE_PLAY_STOP] = False
if self.chars[FEATURE_TOGGLE_MUTE]:
current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)
if not self._flag[FEATURE_TOGGLE_MUTE]:
_LOGGER.debug('%s: Set current state for "toggle_mute" to %s',
self.entity_id, current_state)
self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state)
self._flag[FEATURE_TOGGLE_MUTE] = False

View File

@ -1,25 +1,60 @@
"""Class to hold all switch accessories.""" """Class to hold all switch accessories."""
import logging import logging
from pyhap.const import CATEGORY_SWITCH from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH
from homeassistant.components.switch import DOMAIN as SWITCH
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON)
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
from . import TYPES from . import TYPES
from .accessories import HomeAccessory from .accessories import HomeAccessory
from .const import SERV_SWITCH, CHAR_ON from .const import CHAR_ON, CHAR_OUTLET_IN_USE, SERV_OUTLET, SERV_SWITCH
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@TYPES.register('Outlet')
class Outlet(HomeAccessory):
"""Generate an Outlet accessory."""
def __init__(self, *args):
"""Initialize an Outlet accessory object."""
super().__init__(*args, category=CATEGORY_OUTLET)
self.flag_target_state = False
serv_outlet = self.add_preload_service(SERV_OUTLET)
self.char_on = serv_outlet.configure_char(
CHAR_ON, value=False, setter_callback=self.set_state)
self.char_outlet_in_use = serv_outlet.configure_char(
CHAR_OUTLET_IN_USE, value=True)
def set_state(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state to %s',
self.entity_id, value)
self.flag_target_state = True
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self.hass.services.call(SWITCH, service, params)
def update_state(self, new_state):
"""Update switch state after state changed."""
current_state = (new_state.state == STATE_ON)
if not self.flag_target_state:
_LOGGER.debug('%s: Set current state to %s',
self.entity_id, current_state)
self.char_on.set_value(current_state)
self.flag_target_state = False
@TYPES.register('Switch') @TYPES.register('Switch')
class Switch(HomeAccessory): class Switch(HomeAccessory):
"""Generate a Switch accessory.""" """Generate a Switch accessory."""
def __init__(self, *args): def __init__(self, *args):
"""Initialize a Switch accessory object to represent a remote.""" """Initialize a Switch accessory object."""
super().__init__(*args, category=CATEGORY_SWITCH) super().__init__(*args, category=CATEGORY_SWITCH)
self._domain = split_entity_id(self.entity_id)[0] self._domain = split_entity_id(self.entity_id)[0]
self.flag_target_state = False self.flag_target_state = False

View File

@ -4,15 +4,16 @@ import logging
from pyhap.const import CATEGORY_THERMOSTAT from pyhap.const import CATEGORY_THERMOSTAT
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP,
ATTR_OPERATION_LIST, ATTR_OPERATION_MODE,
ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP,
DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO, DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO,
STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH,
SUPPORT_TARGET_TEMPERATURE_LOW) SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
TEMP_CELSIUS, TEMP_FAHRENHEIT)
from . import TYPES from . import TYPES
from .accessories import debounce, HomeAccessory from .accessories import debounce, HomeAccessory
@ -20,7 +21,7 @@ from .const import (
CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING,
CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING,
CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE,
CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT) CHAR_TEMP_DISPLAY_UNITS, PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_THERMOSTAT)
from .util import temperature_to_homekit, temperature_to_states from .util import temperature_to_homekit, temperature_to_states
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -42,17 +43,18 @@ class Thermostat(HomeAccessory):
def __init__(self, *args): def __init__(self, *args):
"""Initialize a Thermostat accessory object.""" """Initialize a Thermostat accessory object."""
super().__init__(*args, category=CATEGORY_THERMOSTAT) super().__init__(*args, category=CATEGORY_THERMOSTAT)
self._unit = TEMP_CELSIUS self._unit = self.hass.config.units.temperature_unit
self.support_power_state = False self.support_power_state = False
self.heat_cool_flag_target_state = False self.heat_cool_flag_target_state = False
self.temperature_flag_target_state = False self.temperature_flag_target_state = False
self.coolingthresh_flag_target_state = False self.coolingthresh_flag_target_state = False
self.heatingthresh_flag_target_state = False self.heatingthresh_flag_target_state = False
min_temp, max_temp = self.get_temperature_range()
# Add additional characteristics if auto mode is supported # Add additional characteristics if auto mode is supported
self.chars = [] self.chars = []
features = self.hass.states.get(self.entity_id) \ features = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_SUPPORTED_FEATURES) .attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if features & SUPPORT_ON_OFF: if features & SUPPORT_ON_OFF:
self.support_power_state = True self.support_power_state = True
if features & SUPPORT_TEMP_RANGE: if features & SUPPORT_TEMP_RANGE:
@ -73,6 +75,8 @@ class Thermostat(HomeAccessory):
CHAR_CURRENT_TEMPERATURE, value=21.0) CHAR_CURRENT_TEMPERATURE, value=21.0)
self.char_target_temp = serv_thermostat.configure_char( self.char_target_temp = serv_thermostat.configure_char(
CHAR_TARGET_TEMPERATURE, value=21.0, CHAR_TARGET_TEMPERATURE, value=21.0,
properties={PROP_MIN_VALUE: min_temp,
PROP_MAX_VALUE: max_temp},
setter_callback=self.set_target_temperature) setter_callback=self.set_target_temperature)
# Display units characteristic # Display units characteristic
@ -85,12 +89,30 @@ class Thermostat(HomeAccessory):
if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars: if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars:
self.char_cooling_thresh_temp = serv_thermostat.configure_char( self.char_cooling_thresh_temp = serv_thermostat.configure_char(
CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0, CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0,
properties={PROP_MIN_VALUE: min_temp,
PROP_MAX_VALUE: max_temp},
setter_callback=self.set_cooling_threshold) setter_callback=self.set_cooling_threshold)
if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars: if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars:
self.char_heating_thresh_temp = serv_thermostat.configure_char( self.char_heating_thresh_temp = serv_thermostat.configure_char(
CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0, CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0,
properties={PROP_MIN_VALUE: min_temp,
PROP_MAX_VALUE: max_temp},
setter_callback=self.set_heating_threshold) setter_callback=self.set_heating_threshold)
def get_temperature_range(self):
"""Return min and max temperature range."""
max_temp = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MAX_TEMP)
max_temp = temperature_to_homekit(max_temp, self._unit) if max_temp \
else DEFAULT_MAX_TEMP
min_temp = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MIN_TEMP)
min_temp = temperature_to_homekit(min_temp, self._unit) if min_temp \
else DEFAULT_MIN_TEMP
return min_temp, max_temp
def set_heat_cool(self, value): def set_heat_cool(self, value):
"""Move operation mode to value if call came from HomeKit.""" """Move operation mode to value if call came from HomeKit."""
if value in HC_HOMEKIT_TO_HASS: if value in HC_HOMEKIT_TO_HASS:
@ -147,9 +169,6 @@ class Thermostat(HomeAccessory):
def update_state(self, new_state): def update_state(self, new_state):
"""Update security state after state changed.""" """Update security state after state changed."""
self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS)
# Update current temperature # Update current temperature
current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE)
if isinstance(current_temp, (int, float)): if isinstance(current_temp, (int, float)):

View File

@ -3,41 +3,108 @@ import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.components.media_player as media_player
from homeassistant.core import split_entity_id from homeassistant.core import split_entity_id
from homeassistant.const import ( from homeassistant.const import (
ATTR_CODE, CONF_NAME, TEMP_CELSIUS) ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, TEMP_CELSIUS)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.temperature as temp_util import homeassistant.util.temperature as temp_util
from .const import HOMEKIT_NOTIFY_ID from .const import (
CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF,
FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_OUTLET,
TYPE_SWITCH)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
BASIC_INFO_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
})
FEATURE_SCHEMA = BASIC_INFO_SCHEMA.extend({
vol.Optional(CONF_FEATURE_LIST, default=None): cv.ensure_list,
})
CODE_SCHEMA = BASIC_INFO_SCHEMA.extend({
vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string),
})
MEDIA_PLAYER_SCHEMA = vol.Schema({
vol.Required(CONF_FEATURE): vol.All(
cv.string, vol.In((FEATURE_ON_OFF, FEATURE_PLAY_PAUSE,
FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE))),
})
SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend({
vol.Optional(CONF_TYPE, default=TYPE_SWITCH): vol.All(
cv.string, vol.In((TYPE_OUTLET, TYPE_SWITCH))),
})
def validate_entity_config(values): def validate_entity_config(values):
"""Validate config entry for CONF_ENTITY.""" """Validate config entry for CONF_ENTITY."""
entities = {} entities = {}
for entity_id, config in values.items(): for entity_id, config in values.items():
entity = cv.entity_id(entity_id) entity = cv.entity_id(entity_id)
params = {}
if not isinstance(config, dict):
raise vol.Invalid('The configuration for "{}" must be '
' a dictionary.'.format(entity))
for key in (CONF_NAME, ):
value = config.get(key, -1)
if value != -1:
params[key] = cv.string(value)
domain, _ = split_entity_id(entity) domain, _ = split_entity_id(entity)
if domain in ('alarm_control_panel', 'lock'): if not isinstance(config, dict):
code = config.get(ATTR_CODE) raise vol.Invalid('The configuration for {} must be '
params[ATTR_CODE] = cv.string(code) if code else None ' a dictionary.'.format(entity))
entities[entity] = params if domain in ('alarm_control_panel', 'lock'):
config = CODE_SCHEMA(config)
elif domain == media_player.DOMAIN:
config = FEATURE_SCHEMA(config)
feature_list = {}
for feature in config[CONF_FEATURE_LIST]:
params = MEDIA_PLAYER_SCHEMA(feature)
key = params.pop(CONF_FEATURE)
if key in feature_list:
raise vol.Invalid('A feature can be added only once for {}'
.format(entity))
feature_list[key] = params
config[CONF_FEATURE_LIST] = feature_list
elif domain == 'switch':
config = SWITCH_TYPE_SCHEMA(config)
else:
config = BASIC_INFO_SCHEMA(config)
entities[entity] = config
return entities return entities
def validate_media_player_features(state, feature_list):
"""Validate features for media players."""
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
supported_modes = []
if features & (media_player.SUPPORT_TURN_ON |
media_player.SUPPORT_TURN_OFF):
supported_modes.append(FEATURE_ON_OFF)
if features & (media_player.SUPPORT_PLAY | media_player.SUPPORT_PAUSE):
supported_modes.append(FEATURE_PLAY_PAUSE)
if features & (media_player.SUPPORT_PLAY | media_player.SUPPORT_STOP):
supported_modes.append(FEATURE_PLAY_STOP)
if features & media_player.SUPPORT_VOLUME_MUTE:
supported_modes.append(FEATURE_TOGGLE_MUTE)
error_list = []
for feature in feature_list:
if feature not in supported_modes:
error_list.append(feature)
if error_list:
_LOGGER.error("%s does not support features: %s",
state.entity_id, error_list)
return False
return True
def show_setup_message(hass, pincode): def show_setup_message(hass, pincode):
"""Display persistent notification with setup information.""" """Display persistent notification with setup information."""
pin = pincode.decode() pin = pincode.decode()

View File

@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['pyhomematic==0.1.42'] REQUIREMENTS = ['pyhomematic==0.1.43']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -17,7 +17,7 @@ from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.core import callback from homeassistant.core import callback
REQUIREMENTS = ['homematicip==0.9.2.4'] REQUIREMENTS = ['homematicip==0.9.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -27,7 +27,8 @@ COMPONENTS = [
'sensor', 'sensor',
'binary_sensor', 'binary_sensor',
'switch', 'switch',
'light' 'light',
'climate',
] ]
CONF_NAME = 'name' CONF_NAME = 'name'

View File

@ -0,0 +1,153 @@
"""
Support for Hydrawise cloud.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/hydrawise/
"""
import asyncio
from datetime import timedelta
import logging
from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol
from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL)
import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, dispatcher_send)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_interval
REQUIREMENTS = ['hydrawiser==0.1.1']
_LOGGER = logging.getLogger(__name__)
ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60]
CONF_ATTRIBUTION = "Data provided by hydrawise.com"
CONF_WATERING_TIME = 'watering_minutes'
NOTIFICATION_ID = 'hydrawise_notification'
NOTIFICATION_TITLE = 'Hydrawise Setup'
DATA_HYDRAWISE = 'hydrawise'
DOMAIN = 'hydrawise'
DEFAULT_WATERING_TIME = 15
DEVICE_MAP_INDEX = ['KEY_INDEX', 'ICON_INDEX', 'DEVICE_CLASS_INDEX',
'UNIT_OF_MEASURE_INDEX']
DEVICE_MAP = {
'auto_watering': ['Automatic Watering', 'mdi:autorenew', '', ''],
'is_watering': ['Watering', '', 'moisture', ''],
'manual_watering': ['Manual Watering', 'mdi:water-pump', '', ''],
'next_cycle': ['Next Cycle', 'mdi:calendar-clock', '', ''],
'status': ['Status', '', 'connectivity', ''],
'watering_time': ['Watering Time', 'mdi:water-pump', '', 'min'],
'rain_sensor': ['Rain Sensor', '', 'moisture', '']
}
BINARY_SENSORS = ['is_watering', 'status', 'rain_sensor']
SENSORS = ['next_cycle', 'watering_time']
SWITCHES = ['auto_watering', 'manual_watering']
SCAN_INTERVAL = timedelta(seconds=30)
SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Hunter Hydrawise component."""
conf = config[DOMAIN]
access_token = conf[CONF_ACCESS_TOKEN]
scan_interval = conf.get(CONF_SCAN_INTERVAL)
try:
from hydrawiser.core import Hydrawiser
hydrawise = Hydrawiser(user_token=access_token)
hass.data[DATA_HYDRAWISE] = HydrawiseHub(hydrawise)
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error(
"Unable to connect to Hydrawise cloud service: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
def hub_refresh(event_time):
"""Call Hydrawise hub to refresh information."""
_LOGGER.debug("Updating Hydrawise Hub component")
hass.data[DATA_HYDRAWISE].data.update_controller_info()
dispatcher_send(hass, SIGNAL_UPDATE_HYDRAWISE)
# Call the Hydrawise API to refresh updates
track_time_interval(hass, hub_refresh, scan_interval)
return True
class HydrawiseHub(object):
"""Representation of a base Hydrawise device."""
def __init__(self, data):
"""Initialize the entity."""
self.data = data
class HydrawiseEntity(Entity):
"""Entity class for Hydrawise devices."""
def __init__(self, data, sensor_type):
"""Initialize the Hydrawise entity."""
self.data = data
self._sensor_type = sensor_type
self._name = "{0} {1}".format(
self.data['name'],
DEVICE_MAP[self._sensor_type][
DEVICE_MAP_INDEX.index('KEY_INDEX')])
self._state = None
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
async_dispatcher_connect(
self.hass, SIGNAL_UPDATE_HYDRAWISE, self._update_callback)
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
@property
def unit_of_measurement(self):
"""Return the units of measurement."""
return DEVICE_MAP[self._sensor_type][
DEVICE_MAP_INDEX.index('UNIT_OF_MEASURE_INDEX')]
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
'identifier': self.data.get('relay'),
}

View File

@ -50,10 +50,7 @@ def setup(hass, config):
"""Set up the keyboard_remote.""" """Set up the keyboard_remote."""
config = config.get(DOMAIN) config = config.get(DOMAIN)
keyboard_remote = KeyboardRemote( keyboard_remote = KeyboardRemote(hass, config)
hass,
config
)
def _start_keyboard_remote(_event): def _start_keyboard_remote(_event):
keyboard_remote.run() keyboard_remote.run()
@ -61,14 +58,8 @@ def setup(hass, config):
def _stop_keyboard_remote(_event): def _stop_keyboard_remote(_event):
keyboard_remote.stop() keyboard_remote.stop()
hass.bus.listen_once( hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_keyboard_remote)
EVENT_HOMEASSISTANT_START, hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_keyboard_remote)
_start_keyboard_remote
)
hass.bus.listen_once(
EVENT_HOMEASSISTANT_STOP,
_stop_keyboard_remote
)
return True return True
@ -93,10 +84,8 @@ class KeyboardRemoteThread(threading.Thread):
_LOGGER.debug("Keyboard connected, %s", self.device_id) _LOGGER.debug("Keyboard connected, %s", self.device_id)
else: else:
_LOGGER.debug( _LOGGER.debug(
'Keyboard not connected, %s.\n\ "Keyboard not connected, %s. "
Check /dev/input/event* permissions.', "Check /dev/input/event* permissions", self.device_id)
self.device_id
)
id_folder = '/dev/input/by-id/' id_folder = '/dev/input/by-id/'
@ -105,12 +94,9 @@ class KeyboardRemoteThread(threading.Thread):
device_names = [InputDevice(file_name).name device_names = [InputDevice(file_name).name
for file_name in list_devices()] for file_name in list_devices()]
_LOGGER.debug( _LOGGER.debug(
'Possible device names are:\n %s.\n \ "Possible device names are: %s. "
Possible device descriptors are %s:\n %s', "Possible device descriptors are %s: %s",
device_names, device_names, id_folder, os.listdir(id_folder))
id_folder,
os.listdir(id_folder)
)
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.stopped = threading.Event() self.stopped = threading.Event()
@ -149,9 +135,7 @@ class KeyboardRemoteThread(threading.Thread):
self.dev = self._get_keyboard_device() self.dev = self._get_keyboard_device()
if self.dev is not None: if self.dev is not None:
self.dev.grab() self.dev.grab()
self.hass.bus.fire( self.hass.bus.fire(KEYBOARD_REMOTE_CONNECTED)
KEYBOARD_REMOTE_CONNECTED
)
_LOGGER.debug("Keyboard re-connected, %s", self.device_id) _LOGGER.debug("Keyboard re-connected, %s", self.device_id)
else: else:
continue continue
@ -160,9 +144,7 @@ class KeyboardRemoteThread(threading.Thread):
event = self.dev.read_one() event = self.dev.read_one()
except IOError: # Keyboard Disconnected except IOError: # Keyboard Disconnected
self.dev = None self.dev = None
self.hass.bus.fire( self.hass.bus.fire(KEYBOARD_REMOTE_DISCONNECTED)
KEYBOARD_REMOTE_DISCONNECTED
)
_LOGGER.debug("Keyboard disconnected, %s", self.device_id) _LOGGER.debug("Keyboard disconnected, %s", self.device_id)
continue continue
@ -174,7 +156,11 @@ class KeyboardRemoteThread(threading.Thread):
_LOGGER.debug(categorize(event)) _LOGGER.debug(categorize(event))
self.hass.bus.fire( self.hass.bus.fire(
KEYBOARD_REMOTE_COMMAND_RECEIVED, KEYBOARD_REMOTE_COMMAND_RECEIVED,
{KEY_CODE: event.code} {
KEY_CODE: event.code,
DEVICE_DESCRIPTOR: self.device_descriptor,
DEVICE_NAME: self.device_name
}
) )
@ -191,9 +177,8 @@ class KeyboardRemote(object):
if device_descriptor is not None\ if device_descriptor is not None\
or device_name is not None: or device_name is not None:
thread = KeyboardRemoteThread(hass, device_name, thread = KeyboardRemoteThread(
device_descriptor, hass, device_name, device_descriptor, key_value)
key_value)
self.threads.append(thread) self.threads.append(thread)
def run(self): def run(self):

View File

@ -0,0 +1,158 @@
"""
Support for Lagute LW-12 WiFi LED Controller.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.lw12wifi/
"""
import logging
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION,
Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT,
SUPPORT_COLOR, SUPPORT_TRANSITION
)
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT
)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
REQUIREMENTS = ['lw12==0.9.2']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'LW-12 FC'
DEFAULT_PORT = 5000
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup LW-12 WiFi LED Controller platform."""
import lw12
# Assign configuration variables.
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
# Add devices
lw12_light = lw12.LW12Controller(host, port)
add_devices([LW12WiFi(name, lw12_light)])
class LW12WiFi(Light):
"""LW-12 WiFi LED Controller."""
def __init__(self, name, lw12_light):
"""Initialisation of LW-12 WiFi LED Controller.
Args:
name: Friendly name for this platform to use.
lw12_light: Instance of the LW12 controller.
"""
self._light = lw12_light
self._name = name
self._state = None
self._effect = None
self._rgb_color = [255, 255, 255]
self._brightness = 255
# Setup feature list
self._supported_features = SUPPORT_BRIGHTNESS | SUPPORT_EFFECT \
| SUPPORT_COLOR | SUPPORT_TRANSITION
@property
def name(self):
"""Return the display name of the controlled light."""
return self._name
@property
def brightness(self):
"""Return the brightness of the light."""
return self._brightness
@property
def hs_color(self):
"""Read back the hue-saturation of the light."""
return color_util.color_RGB_to_hs(*self._rgb_color)
@property
def effect(self):
"""Return current light effect."""
if self._effect is None:
return None
return self._effect.replace('_', ' ').title()
@property
def is_on(self):
"""Return true if light is on."""
return self._state
@property
def supported_features(self):
"""Return a list of supported features."""
return self._supported_features
@property
def effect_list(self):
"""Return a list of available effects.
Use the Enum element name for display.
"""
import lw12
return [effect.name.replace('_', ' ').title()
for effect in lw12.LW12_EFFECT]
@property
def assumed_state(self) -> bool:
"""Return True if unable to access real state of the entity."""
return True
@property
def shoud_poll(self) -> bool:
"""Return False to not poll the state of this entity."""
return False
def turn_on(self, **kwargs):
"""Instruct the light to turn on."""
import lw12
self._light.light_on()
if ATTR_HS_COLOR in kwargs:
self._rgb_color = color_util.color_hs_to_RGB(
*kwargs[ATTR_HS_COLOR])
self._light.set_color(*self._rgb_color)
self._effect = None
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs.get(ATTR_BRIGHTNESS)
brightness = int(self._brightness / 255 * 100)
self._light.set_light_option(lw12.LW12_LIGHT.BRIGHTNESS,
brightness)
if ATTR_EFFECT in kwargs:
self._effect = kwargs[ATTR_EFFECT].replace(' ', '_').upper()
# Check if a known and supported effect was selected.
if self._effect in [eff.name for eff in lw12.LW12_EFFECT]:
# Selected effect is supported and will be applied.
self._light.set_effect(lw12.LW12_EFFECT[self._effect])
else:
# Unknown effect was set, recover by disabling the effect
# mode and log an error.
_LOGGER.error("Unknown effect selected: %s", self._effect)
self._effect = None
if ATTR_TRANSITION in kwargs:
transition_speed = int(kwargs[ATTR_TRANSITION])
self._light.set_light_option(lw12.LW12_LIGHT.FLASH,
transition_speed)
self._state = True
def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
self._light.light_off()
self._state = False

View File

@ -17,6 +17,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.util import color as color_util from homeassistant.util import color as color_util
from homeassistant.util.color import \ from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin color_temperature_mired_to_kelvin as mired_to_kelvin
from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['nanoleaf==0.4.1'] REQUIREMENTS = ['nanoleaf==0.4.1']
@ -24,6 +25,10 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Aurora' DEFAULT_NAME = 'Aurora'
DATA_NANOLEAF_AURORA = 'nanoleaf_aurora'
CONFIG_FILE = '.nanoleaf_aurora.conf'
ICON = 'mdi:triangle-outline' ICON = 'mdi:triangle-outline'
SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
@ -39,31 +44,59 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Nanoleaf Aurora device.""" """Set up the Nanoleaf Aurora device."""
import nanoleaf import nanoleaf
host = config.get(CONF_HOST) import nanoleaf.setup
name = config.get(CONF_NAME) if DATA_NANOLEAF_AURORA not in hass.data:
token = config.get(CONF_TOKEN) hass.data[DATA_NANOLEAF_AURORA] = dict()
token = ''
if discovery_info is not None:
host = discovery_info['host']
name = discovery_info['hostname']
# if device already exists via config, skip discovery setup
if host in hass.data[DATA_NANOLEAF_AURORA]:
return
_LOGGER.info("Discovered a new Aurora: %s", discovery_info)
conf = load_json(hass.config.path(CONFIG_FILE))
if conf.get(host, {}).get('token'):
token = conf[host]['token']
else:
host = config[CONF_HOST]
name = config[CONF_NAME]
token = config[CONF_TOKEN]
if not token:
token = nanoleaf.setup.generate_auth_token(host)
if not token:
_LOGGER.error("Could not generate the auth token, did you press "
"and hold the power button on %s"
"for 5-7 seconds?", name)
return
conf = load_json(hass.config.path(CONFIG_FILE))
conf[host] = {'token': token}
save_json(hass.config.path(CONFIG_FILE), conf)
aurora_light = nanoleaf.Aurora(host, token) aurora_light = nanoleaf.Aurora(host, token)
aurora_light.hass_name = name
if aurora_light.on is None: if aurora_light.on is None:
_LOGGER.error( _LOGGER.error(
"Could not connect to Nanoleaf Aurora: %s on %s", name, host) "Could not connect to Nanoleaf Aurora: %s on %s", name, host)
return return
add_devices([AuroraLight(aurora_light)], True) hass.data[DATA_NANOLEAF_AURORA][host] = aurora_light
add_devices([AuroraLight(aurora_light, name)], True)
class AuroraLight(Light): class AuroraLight(Light):
"""Representation of a Nanoleaf Aurora.""" """Representation of a Nanoleaf Aurora."""
def __init__(self, light): def __init__(self, light, name):
"""Initialize an Aurora light.""" """Initialize an Aurora light."""
self._brightness = None self._brightness = None
self._color_temp = None self._color_temp = None
self._effect = None self._effect = None
self._effects_list = None self._effects_list = None
self._light = light self._light = light
self._name = light.hass_name self._name = name
self._hs_color = None self._hs_color = None
self._state = None self._state = None

View File

@ -27,8 +27,10 @@ REQUIREMENTS = ['lightify==1.0.6.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ALLOW_LIGHTIFY_NODES = 'allow_lightify_nodes'
CONF_ALLOW_LIGHTIFY_GROUPS = 'allow_lightify_groups' CONF_ALLOW_LIGHTIFY_GROUPS = 'allow_lightify_groups'
DEFAULT_ALLOW_LIGHTIFY_NODES = True
DEFAULT_ALLOW_LIGHTIFY_GROUPS = True DEFAULT_ALLOW_LIGHTIFY_GROUPS = True
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
@ -40,6 +42,8 @@ SUPPORT_OSRAMLIGHTIFY = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP |
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_ALLOW_LIGHTIFY_NODES,
default=DEFAULT_ALLOW_LIGHTIFY_NODES): cv.boolean,
vol.Optional(CONF_ALLOW_LIGHTIFY_GROUPS, vol.Optional(CONF_ALLOW_LIGHTIFY_GROUPS,
default=DEFAULT_ALLOW_LIGHTIFY_GROUPS): cv.boolean, default=DEFAULT_ALLOW_LIGHTIFY_GROUPS): cv.boolean,
}) })
@ -50,6 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
import lightify import lightify
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
add_nodes = config.get(CONF_ALLOW_LIGHTIFY_NODES)
add_groups = config.get(CONF_ALLOW_LIGHTIFY_GROUPS) add_groups = config.get(CONF_ALLOW_LIGHTIFY_GROUPS)
try: try:
@ -60,10 +65,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.exception(msg) _LOGGER.exception(msg)
return return
setup_bridge(bridge, add_devices, add_groups) setup_bridge(bridge, add_devices, add_nodes, add_groups)
def setup_bridge(bridge, add_devices, add_groups): def setup_bridge(bridge, add_devices, add_nodes, add_groups):
"""Set up the Lightify bridge.""" """Set up the Lightify bridge."""
lights = {} lights = {}
@ -80,6 +85,7 @@ def setup_bridge(bridge, add_devices, add_groups):
new_lights = [] new_lights = []
if add_nodes:
for (light_id, light) in bridge.lights().items(): for (light_id, light) in bridge.lights().items():
if light_id not in lights: if light_id not in lights:
osram_light = OsramLightifyLight( osram_light = OsramLightifyLight(

View File

@ -172,7 +172,8 @@ class Light(zha.Entity, light.Light):
result = await zha.safe_read(self._endpoint.light_color, result = await zha.safe_read(self._endpoint.light_color,
['current_x', 'current_y']) ['current_x', 'current_y'])
if 'current_x' in result and 'current_y' in result: if 'current_x' in result and 'current_y' in result:
xy_color = (result['current_x'], result['current_y']) xy_color = (round(result['current_x']/65535, 3),
round(result['current_y']/65535, 3))
self._hs_color = color_util.color_xy_to_hs(*xy_color) self._hs_color = color_util.color_xy_to_hs(*xy_color)
@property @property

View File

@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['linode-api==4.1.4b2'] REQUIREMENTS = ['linode-api==4.1.9b1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,92 @@
"""
Support for Xiaomi Aqara Lock.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/lock.xiaomi_aqara/
"""
import logging
from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY,
XiaomiDevice)
from homeassistant.components.lock import LockDevice
from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED)
from homeassistant.helpers.event import async_call_later
from homeassistant.core import callback
_LOGGER = logging.getLogger(__name__)
FINGER_KEY = 'fing_verified'
PASSWORD_KEY = 'psw_verified'
CARD_KEY = 'card_verified'
VERIFIED_WRONG_KEY = 'verified_wrong'
ATTR_VERIFIED_WRONG_TIMES = 'verified_wrong_times'
UNLOCK_MAINTAIN_TIME = 5
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Perform the setup for Xiaomi devices."""
devices = []
for gateway in hass.data[PY_XIAOMI_GATEWAY].gateways.values():
for device in gateway.devices['lock']:
model = device['model']
if model == 'lock.aq1':
devices.append(XiaomiAqaraLock(device, 'Lock', gateway))
async_add_devices(devices)
class XiaomiAqaraLock(LockDevice, XiaomiDevice):
"""Representation of a XiaomiAqaraLock."""
def __init__(self, device, name, xiaomi_hub):
"""Initialize the XiaomiAqaraLock."""
self._changed_by = 0
self._verified_wrong_times = 0
super().__init__(device, name, xiaomi_hub)
@property
def is_locked(self) -> bool:
"""Return true if lock is locked."""
if self._state is not None:
return self._state == STATE_LOCKED
@property
def changed_by(self) -> int:
"""Last change triggered by."""
return self._changed_by
@property
def device_state_attributes(self) -> dict:
"""Return the state attributes."""
attributes = {
ATTR_VERIFIED_WRONG_TIMES: self._verified_wrong_times,
}
return attributes
@callback
def clear_unlock_state(self, _):
"""Clear unlock state automatically."""
self._state = STATE_LOCKED
self.async_schedule_update_ha_state()
def parse_data(self, data, raw_data):
"""Parse data sent by gateway."""
value = data.get(VERIFIED_WRONG_KEY)
if value is not None:
self._verified_wrong_times = int(value)
return True
for key in (FINGER_KEY, PASSWORD_KEY, CARD_KEY):
value = data.get(key)
if value is not None:
self._changed_by = int(value)
self._verified_wrong_times = 0
self._state = STATE_UNLOCKED
async_call_later(self.hass, UNLOCK_MAINTAIN_TIME,
self.clear_unlock_state)
return True
return False

View File

@ -100,7 +100,7 @@ async def setup(hass, config):
hass.http.register_view(LogbookView(config.get(DOMAIN, {}))) hass.http.register_view(LogbookView(config.get(DOMAIN, {})))
await hass.components.frontend.async_register_built_in_panel( await hass.components.frontend.async_register_built_in_panel(
'logbook', 'logbook', 'mdi:format-list-bulleted-type') 'logbook', 'logbook', 'hass:format-list-bulleted-type')
hass.services.async_register( hass.services.async_register(
DOMAIN, 'log', log_message, schema=LOG_MESSAGE_SCHEMA) DOMAIN, 'log', log_message, schema=LOG_MESSAGE_SCHEMA)

View File

@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA) SERVICE_PLAY_MEDIA)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2018.05.09'] REQUIREMENTS = ['youtube_dl==2018.06.02']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -20,7 +20,7 @@ from homeassistant.const import (
CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT) CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.6.1'] REQUIREMENTS = ['denonavr==0.7.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
CONF_DEVICE, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING, CONF_PORT) CONF_DEVICE, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING, CONF_PORT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['directpy==0.2'] REQUIREMENTS = ['directpy==0.5']
DEFAULT_DEVICE = '0' DEFAULT_DEVICE = '0'
DEFAULT_NAME = 'DirecTV Receiver' DEFAULT_NAME = 'DirecTV Receiver'

View File

@ -294,6 +294,7 @@ class KodiDevice(MediaPlayerDevice):
# Register notification listeners # Register notification listeners
self._ws_server.Player.OnPause = self.async_on_speed_event self._ws_server.Player.OnPause = self.async_on_speed_event
self._ws_server.Player.OnPlay = self.async_on_speed_event self._ws_server.Player.OnPlay = self.async_on_speed_event
self._ws_server.Player.OnResume = self.async_on_speed_event
self._ws_server.Player.OnSpeedChanged = self.async_on_speed_event self._ws_server.Player.OnSpeedChanged = self.async_on_speed_event
self._ws_server.Player.OnStop = self.async_on_stop self._ws_server.Player.OnStop = self.async_on_stop
self._ws_server.Application.OnVolumeChanged = \ self._ws_server.Application.OnVolumeChanged = \
@ -541,8 +542,8 @@ class KodiDevice(MediaPlayerDevice):
def media_title(self): def media_title(self):
"""Title of current playing media.""" """Title of current playing media."""
# find a string we can use as a title # find a string we can use as a title
return self._item.get( item = self._item
'title', self._item.get('label', self._item.get('file'))) return item.get('title') or item.get('label') or item.get('file')
@property @property
def media_series_title(self): def media_series_title(self):

View File

@ -13,20 +13,22 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, MediaPlayerDevice) SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP,
SUPPORT_PLAY, MediaPlayerDevice)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_API_VERSION, STATE_OFF, STATE_ON, STATE_UNKNOWN) CONF_HOST, CONF_NAME, CONF_API_VERSION, STATE_OFF, STATE_ON, STATE_UNKNOWN)
from homeassistant.helpers.script import Script from homeassistant.helpers.script import Script
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['ha-philipsjs==0.0.3'] REQUIREMENTS = ['ha-philipsjs==0.0.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_SELECT_SOURCE
SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \ SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY
@ -165,6 +167,10 @@ class PhilipsTV(MediaPlayerDevice):
if not self._tv.on: if not self._tv.on:
self._state = STATE_OFF self._state = STATE_OFF
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self._tv.setVolume(volume)
def media_previous_track(self): def media_previous_track(self):
"""Send rewind command.""" """Send rewind command."""
self._tv.sendKey('Previous') self._tv.sendKey('Previous')
@ -189,12 +195,10 @@ class PhilipsTV(MediaPlayerDevice):
self._volume = self._tv.volume self._volume = self._tv.volume
self._muted = self._tv.muted self._muted = self._tv.muted
if self._tv.source_id: if self._tv.source_id:
src = self._tv.sources.get(self._tv.source_id, None) self._source = self._tv.getSourceName(self._tv.source_id)
if src:
self._source = src.get('name', None)
if self._tv.sources and not self._source_list: if self._tv.sources and not self._source_list:
for srcid in sorted(self._tv.sources): for srcid in self._tv.sources:
srcname = self._tv.sources.get(srcid, dict()).get('name', None) srcname = self._tv.getSourceName(srcid)
self._source_list.append(srcname) self._source_list.append(srcname)
self._source_mapping[srcname] = srcid self._source_mapping[srcname] = srcid
if self._tv.on: if self._tv.on:

View File

@ -4,18 +4,22 @@ Support for Nest devices.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/nest/ https://home-assistant.io/components/nest/
""" """
from concurrent.futures import ThreadPoolExecutor
import logging import logging
import socket import socket
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.const import ( from homeassistant.const import (
CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS,
CONF_MONITORED_CONDITIONS) CONF_MONITORED_CONDITIONS,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import discovery, config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send, \
async_dispatcher_connect
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['python-nest==3.7.0'] REQUIREMENTS = ['python-nest==4.0.1']
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -24,6 +28,8 @@ DOMAIN = 'nest'
DATA_NEST = 'nest' DATA_NEST = 'nest'
SIGNAL_NEST_UPDATE = 'nest_update'
NEST_CONFIG_FILE = 'nest.conf' NEST_CONFIG_FILE = 'nest.conf'
CONF_CLIENT_ID = 'client_id' CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret' CONF_CLIENT_SECRET = 'client_secret'
@ -51,23 +57,44 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
def request_configuration(nest, hass, config): async def async_nest_update_event_broker(hass, nest):
"""
Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data.
nest.update_event.wait will block the thread in most of time,
so specific an executor to save default thread pool.
"""
_LOGGER.debug("listening nest.update_event")
with ThreadPoolExecutor(max_workers=1) as executor:
while True:
await hass.loop.run_in_executor(executor, nest.update_event.wait)
if hass.is_running:
nest.update_event.clear()
_LOGGER.debug("dispatching nest data update")
async_dispatcher_send(hass, SIGNAL_NEST_UPDATE)
else:
return
async def async_request_configuration(nest, hass, config):
"""Request configuration steps from the user.""" """Request configuration steps from the user."""
configurator = hass.components.configurator configurator = hass.components.configurator
if 'nest' in _CONFIGURING: if 'nest' in _CONFIGURING:
_LOGGER.debug("configurator failed") _LOGGER.debug("configurator failed")
configurator.notify_errors( configurator.async_notify_errors(
_CONFIGURING['nest'], "Failed to configure, please try again.") _CONFIGURING['nest'], "Failed to configure, please try again.")
return return
def nest_configuration_callback(data): async def async_nest_config_callback(data):
"""Run when the configuration callback is called.""" """Run when the configuration callback is called."""
_LOGGER.debug("configurator callback") _LOGGER.debug("configurator callback")
pin = data.get('pin') pin = data.get('pin')
setup_nest(hass, nest, config, pin=pin) if await async_setup_nest(hass, nest, config, pin=pin):
# start nest update event listener as we missed startup hook
hass.async_add_job(async_nest_update_event_broker, hass, nest)
_CONFIGURING['nest'] = configurator.request_config( _CONFIGURING['nest'] = configurator.async_request_config(
"Nest", nest_configuration_callback, "Nest", async_nest_config_callback,
description=('To configure Nest, click Request Authorization below, ' description=('To configure Nest, click Request Authorization below, '
'log into your Nest account, ' 'log into your Nest account, '
'and then enter the resulting PIN'), 'and then enter the resulting PIN'),
@ -78,60 +105,47 @@ def request_configuration(nest, hass, config):
) )
def setup_nest(hass, nest, config, pin=None): async def async_setup_nest(hass, nest, config, pin=None):
"""Set up the Nest devices.""" """Set up the Nest devices."""
from nest.nest import AuthorizationError, APIError
if pin is not None: if pin is not None:
_LOGGER.debug("pin acquired, requesting access token") _LOGGER.debug("pin acquired, requesting access token")
error_message = None
try:
nest.request_token(pin) nest.request_token(pin)
except AuthorizationError as auth_error:
error_message = "Nest authorization failed: {}".format(auth_error)
except APIError as api_error:
error_message = "Failed to call Nest API: {}".format(api_error)
if error_message is not None:
_LOGGER.warning(error_message)
hass.components.configurator.async_notify_errors(
_CONFIGURING['nest'], error_message)
return False
if nest.access_token is None: if nest.access_token is None:
_LOGGER.debug("no access_token, requesting configuration") _LOGGER.debug("no access_token, requesting configuration")
request_configuration(nest, hass, config) await async_request_configuration(nest, hass, config)
return return False
if 'nest' in _CONFIGURING: if 'nest' in _CONFIGURING:
_LOGGER.debug("configuration done") _LOGGER.debug("configuration done")
configurator = hass.components.configurator configurator = hass.components.configurator
configurator.request_done(_CONFIGURING.pop('nest')) configurator.async_request_done(_CONFIGURING.pop('nest'))
_LOGGER.debug("proceeding with setup") _LOGGER.debug("proceeding with setup")
conf = config[DOMAIN] conf = config[DOMAIN]
hass.data[DATA_NEST] = NestDevice(hass, conf, nest) hass.data[DATA_NEST] = NestDevice(hass, conf, nest)
_LOGGER.debug("proceeding with discovery") for component, discovered in [
discovery.load_platform(hass, 'climate', DOMAIN, {}, config) ('climate', {}),
discovery.load_platform(hass, 'camera', DOMAIN, {}, config) ('camera', {}),
('sensor', conf.get(CONF_SENSORS, {})),
sensor_config = conf.get(CONF_SENSORS, {}) ('binary_sensor', conf.get(CONF_BINARY_SENSORS, {}))]:
discovery.load_platform(hass, 'sensor', DOMAIN, sensor_config, config) _LOGGER.debug("proceeding with discovery -- %s", component)
hass.async_add_job(discovery.async_load_platform,
binary_sensor_config = conf.get(CONF_BINARY_SENSORS, {}) hass, component, DOMAIN, discovered, config)
discovery.load_platform(hass, 'binary_sensor', DOMAIN,
binary_sensor_config, config)
_LOGGER.debug("setup done")
return True
def setup(hass, config):
"""Set up the Nest thermostat component."""
import nest
if 'nest' in _CONFIGURING:
return
conf = config[DOMAIN]
client_id = conf[CONF_CLIENT_ID]
client_secret = conf[CONF_CLIENT_SECRET]
filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE)
access_token_cache_file = hass.config.path(filename)
nest = nest.Nest(
access_token_cache_file=access_token_cache_file,
client_id=client_id, client_secret=client_secret)
setup_nest(hass, nest, config)
def set_mode(service): def set_mode(service):
"""Set the home/away mode for a Nest structure.""" """Set the home/away mode for a Nest structure."""
@ -148,9 +162,47 @@ def setup(hass, config):
_LOGGER.error("Invalid structure %s", _LOGGER.error("Invalid structure %s",
service.data[ATTR_STRUCTURE]) service.data[ATTR_STRUCTURE])
hass.services.register( hass.services.async_register(
DOMAIN, 'set_mode', set_mode, schema=AWAY_SCHEMA) DOMAIN, 'set_mode', set_mode, schema=AWAY_SCHEMA)
def start_up(event):
"""Start Nest update event listener."""
hass.async_add_job(async_nest_update_event_broker, hass, nest)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up)
def shut_down(event):
"""Stop Nest update event listener."""
if nest:
nest.update_event.set()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down)
_LOGGER.debug("async_setup_nest is done")
return True
async def async_setup(hass, config):
"""Set up Nest components."""
from nest import Nest
if 'nest' in _CONFIGURING:
return
conf = config[DOMAIN]
client_id = conf[CONF_CLIENT_ID]
client_secret = conf[CONF_CLIENT_SECRET]
filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE)
access_token_cache_file = hass.config.path(filename)
nest = Nest(
access_token_cache_file=access_token_cache_file,
client_id=client_id, client_secret=client_secret)
await async_setup_nest(hass, nest, config)
return True return True
@ -168,6 +220,19 @@ class NestDevice(object):
self.local_structure = conf[CONF_STRUCTURE] self.local_structure = conf[CONF_STRUCTURE]
_LOGGER.debug("Structures to include: %s", self.local_structure) _LOGGER.debug("Structures to include: %s", self.local_structure)
def structures(self):
"""Generate a list of structures."""
try:
for structure in self.nest.structures:
if structure.name in self.local_structure:
yield structure
else:
_LOGGER.debug("Ignoring structure %s, not in %s",
structure.name, self.local_structure)
except socket.error:
_LOGGER.error(
"Connection error logging into the nest web service.")
def thermostats(self): def thermostats(self):
"""Generate a list of thermostats and their location.""" """Generate a list of thermostats and their location."""
try: try:
@ -190,7 +255,7 @@ class NestDevice(object):
for device in structure.smoke_co_alarms: for device in structure.smoke_co_alarms:
yield (structure, device) yield (structure, device)
else: else:
_LOGGER.info("Ignoring structure %s, not in %s", _LOGGER.debug("Ignoring structure %s, not in %s",
structure.name, self.local_structure) structure.name, self.local_structure)
except socket.error: except socket.error:
_LOGGER.error( _LOGGER.error(
@ -204,8 +269,59 @@ class NestDevice(object):
for device in structure.cameras: for device in structure.cameras:
yield (structure, device) yield (structure, device)
else: else:
_LOGGER.info("Ignoring structure %s, not in %s", _LOGGER.debug("Ignoring structure %s, not in %s",
structure.name, self.local_structure) structure.name, self.local_structure)
except socket.error: except socket.error:
_LOGGER.error( _LOGGER.error(
"Connection error logging into the nest web service.") "Connection error logging into the nest web service.")
class NestSensorDevice(Entity):
"""Representation of a Nest sensor."""
def __init__(self, structure, device, variable):
"""Initialize the sensor."""
self.structure = structure
self.variable = variable
if device is not None:
# device specific
self.device = device
self._name = "{} {}".format(self.device.name_long,
self.variable.replace('_', ' '))
else:
# structure only
self.device = structure
self._name = "{} {}".format(self.structure.name,
self.variable.replace('_', ' '))
self._state = None
self._unit = None
@property
def name(self):
"""Return the name of the nest, if any."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit
@property
def should_poll(self):
"""Do not need poll thanks using Nest streaming API."""
return False
def update(self):
"""Do not use NestSensorDevice directly."""
raise NotImplementedError
async def async_added_to_hass(self):
"""Register update signal handler."""
async def async_update_state():
"""Update sensor state."""
await self.async_update_ha_state(True)
async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE,
async_update_state)

View File

@ -0,0 +1,61 @@
"""
Flock platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.flock/
"""
import asyncio
import logging
import async_timeout
import voluptuous as vol
from homeassistant.components.notify import (
PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
_RESOURCE = 'https://api.flock.com/hooks/sendMessage/'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ACCESS_TOKEN): cv.string,
})
async def get_service(hass, config, discovery_info=None):
"""Get the Flock notification service."""
access_token = config.get(CONF_ACCESS_TOKEN)
url = '{}{}'.format(_RESOURCE, access_token)
session = async_get_clientsession(hass)
return FlockNotificationService(url, session, hass.loop)
class FlockNotificationService(BaseNotificationService):
"""Implement the notification service for Flock."""
def __init__(self, url, session, loop):
"""Initialize the Flock notification service."""
self._loop = loop
self._url = url
self._session = session
async def async_send_message(self, message, **kwargs):
"""Send the message to the user."""
payload = {'text': message}
_LOGGER.debug("Attempting to call Flock at %s", self._url)
try:
with async_timeout.timeout(10, loop=self._loop):
response = await self._session.post(self._url, json=payload)
result = await response.json()
if response.status != 200 or 'error' in result:
_LOGGER.error(
"Flock service returned HTTP status %d, response %s",
response.status, result)
except asyncio.TimeoutError:
_LOGGER.error("Timeout accessing Flock at %s", self._url)

View File

@ -1,65 +0,0 @@
"""
NMA (Notify My Android) notification service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.nma/
"""
import logging
import xml.etree.ElementTree as ET
import requests
import voluptuous as vol
from homeassistant.components.notify import (
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__)
_RESOURCE = 'https://www.notifymyandroid.com/publicapi/'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
})
def get_service(hass, config, discovery_info=None):
"""Get the NMA notification service."""
parameters = {
'apikey': config[CONF_API_KEY],
}
response = requests.get(
'{}{}'.format(_RESOURCE, 'verify'), params=parameters, timeout=5)
tree = ET.fromstring(response.content)
if tree[0].tag == 'error':
_LOGGER.error("Wrong API key supplied: %s", tree[0].text)
return None
return NmaNotificationService(config[CONF_API_KEY])
class NmaNotificationService(BaseNotificationService):
"""Implement the notification service for NMA."""
def __init__(self, api_key):
"""Initialize the service."""
self._api_key = api_key
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
data = {
'apikey': self._api_key,
'application': 'home-assistant',
'event': kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT),
'description': message,
'priority': 0,
}
response = requests.get(
'{}{}'.format(_RESOURCE, 'notify'), params=data, timeout=5)
tree = ET.fromstring(response.content)
if tree[0].tag == 'error':
_LOGGER.exception(
"Unable to perform request. Error: %s", tree[0].text)

View File

@ -19,7 +19,7 @@ from homeassistant.components.notify import (
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME
from homeassistant.helpers.event import async_track_point_in_time from homeassistant.helpers.event import async_track_point_in_time
REQUIREMENTS = ['TwitterAPI==2.5.0'] REQUIREMENTS = ['TwitterAPI==2.5.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -4,7 +4,6 @@ Register a custom front end panel.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/panel_custom/ https://home-assistant.io/components/panel_custom/
""" """
import asyncio
import logging import logging
import os import os
@ -21,27 +20,33 @@ CONF_SIDEBAR_ICON = 'sidebar_icon'
CONF_URL_PATH = 'url_path' CONF_URL_PATH = 'url_path'
CONF_CONFIG = 'config' CONF_CONFIG = 'config'
CONF_WEBCOMPONENT_PATH = 'webcomponent_path' CONF_WEBCOMPONENT_PATH = 'webcomponent_path'
CONF_JS_URL = 'js_url'
CONF_EMBED_IFRAME = 'embed_iframe'
CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script'
DEFAULT_ICON = 'mdi:bookmark' DEFAULT_ICON = 'mdi:bookmark'
LEGACY_URL = '/api/panel_custom/{}'
PANEL_DIR = 'panels' PANEL_DIR = 'panels'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [{ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_COMPONENT_NAME): cv.slug, vol.Required(CONF_COMPONENT_NAME): cv.string,
vol.Optional(CONF_SIDEBAR_TITLE): cv.string, vol.Optional(CONF_SIDEBAR_TITLE): cv.string,
vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon, vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon,
vol.Optional(CONF_URL_PATH): cv.string, vol.Optional(CONF_URL_PATH): cv.string,
vol.Optional(CONF_CONFIG): cv.match_all, vol.Optional(CONF_CONFIG): dict,
vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile, vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile,
}]) vol.Optional(CONF_JS_URL): cv.string,
vol.Optional(CONF_EMBED_IFRAME, default=False): cv.boolean,
vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT, default=False): cv.boolean,
})])
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Initialize custom panel.""" """Initialize custom panel."""
success = False success = False
@ -52,17 +57,39 @@ def async_setup(hass, config):
if panel_path is None: if panel_path is None:
panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name)) panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name))
if not os.path.isfile(panel_path): custom_panel_config = {
'name': name,
'embed_iframe': panel[CONF_EMBED_IFRAME],
'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT],
}
if CONF_JS_URL in panel:
custom_panel_config['js_url'] = panel[CONF_JS_URL]
elif not await hass.async_add_job(os.path.isfile, panel_path):
_LOGGER.error('Unable to find webcomponent for %s: %s', _LOGGER.error('Unable to find webcomponent for %s: %s',
name, panel_path) name, panel_path)
continue continue
yield from hass.components.frontend.async_register_panel( else:
name, panel_path, url = LEGACY_URL.format(name)
hass.http.register_static_path(url, panel_path)
custom_panel_config['html_url'] = LEGACY_URL.format(name)
if CONF_CONFIG in panel:
# Make copy because we're mutating it
config = dict(panel[CONF_CONFIG])
else:
config = {}
config['_panel_custom'] = custom_panel_config
await hass.components.frontend.async_register_built_in_panel(
component_name='custom',
sidebar_title=panel.get(CONF_SIDEBAR_TITLE), sidebar_title=panel.get(CONF_SIDEBAR_TITLE),
sidebar_icon=panel.get(CONF_SIDEBAR_ICON), sidebar_icon=panel.get(CONF_SIDEBAR_ICON),
frontend_url_path=panel.get(CONF_URL_PATH), frontend_url_path=panel.get(CONF_URL_PATH),
config=panel.get(CONF_CONFIG), config=config
) )
success = True success = True

View File

@ -18,7 +18,7 @@ from homeassistant.loader import bind_hass
from homeassistant.util import sanitize_filename from homeassistant.util import sanitize_filename
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['restrictedpython==4.0b3'] REQUIREMENTS = ['restrictedpython==4.0b4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -11,7 +11,7 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import (CONF_HOST, CONF_PASSWORD) from homeassistant.const import (CONF_HOST, CONF_PASSWORD)
REQUIREMENTS = ['pyrainbird==0.1.3'] REQUIREMENTS = ['pyrainbird==0.1.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -168,7 +168,6 @@ class RainCloudEntity(Entity):
"""Return the state attributes.""" """Return the state attributes."""
return { return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
'current_time': self.data.current_time,
'identifier': self.data.serial, 'identifier': self.data.serial,
} }

View File

@ -1,132 +0,0 @@
"""
This component provides support for RainMachine sprinkler controllers.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/rainmachine/
"""
import logging
import voluptuous as vol
from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL,
CONF_SWITCHES)
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['regenmaschine==0.4.1']
_LOGGER = logging.getLogger(__name__)
DATA_RAINMACHINE = 'data_rainmachine'
DOMAIN = 'rainmachine'
NOTIFICATION_ID = 'rainmachine_notification'
NOTIFICATION_TITLE = 'RainMachine Component Setup'
CONF_ZONE_RUN_TIME = 'zone_run_time'
DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC'
DEFAULT_ICON = 'mdi:water'
DEFAULT_PORT = 8080
DEFAULT_SSL = True
PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN)
SWITCH_SCHEMA = vol.Schema({
vol.Optional(CONF_ZONE_RUN_TIME):
cv.positive_int
})
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema({
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_SWITCHES): SWITCH_SCHEMA,
})
},
extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the RainMachine component."""
from regenmaschine import Authenticator, Client
from regenmaschine.exceptions import HTTPError
from requests.exceptions import ConnectTimeout
conf = config[DOMAIN]
ip_address = conf[CONF_IP_ADDRESS]
password = conf[CONF_PASSWORD]
port = conf[CONF_PORT]
ssl = conf[CONF_SSL]
_LOGGER.debug('Setting up RainMachine client')
try:
auth = Authenticator.create_local(
ip_address, password, port=port, https=ssl)
client = Client(auth)
hass.data[DATA_RAINMACHINE] = RainMachine(client)
except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info:
_LOGGER.error('An error occurred: %s', str(exc_info))
hass.components.persistent_notification.create(
'Error: {0}<br />'
'You will need to restart hass after fixing.'
''.format(exc_info),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
_LOGGER.debug('Setting up switch platform')
switch_config = conf.get(CONF_SWITCHES, {})
discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config)
_LOGGER.debug('Setup complete')
return True
class RainMachine(object):
"""Define a generic RainMachine object."""
def __init__(self, client):
"""Initialize."""
self.client = client
self.device_mac = self.client.provision.wifi()['macAddress']
class RainMachineEntity(Entity):
"""Define a generic RainMachine entity."""
def __init__(self,
rainmachine,
rainmachine_type,
rainmachine_entity_id,
icon=DEFAULT_ICON):
"""Initialize."""
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
self._icon = icon
self._rainmachine_type = rainmachine_type
self._rainmachine_entity_id = rainmachine_entity_id
self.rainmachine = rainmachine
@property
def device_state_attributes(self) -> dict:
"""Return the state attributes."""
return self._attrs
@property
def icon(self) -> str:
"""Return the icon."""
return self._icon
@property
def unique_id(self) -> str:
"""Return a unique, HASS-friendly identifier for this entity."""
return '{0}_{1}_{2}'.format(
self.rainmachine.device_mac.replace(
':', ''), self._rainmachine_type,
self._rainmachine_entity_id)

View File

@ -0,0 +1,226 @@
"""
Support for RainMachine devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/rainmachine/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, CONF_PASSWORD,
CONF_PORT, CONF_SENSORS, CONF_SSL, CONF_MONITORED_CONDITIONS,
CONF_SWITCHES)
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_interval
REQUIREMENTS = ['regenmaschine==0.4.2']
_LOGGER = logging.getLogger(__name__)
DATA_RAINMACHINE = 'data_rainmachine'
DOMAIN = 'rainmachine'
NOTIFICATION_ID = 'rainmachine_notification'
NOTIFICATION_TITLE = 'RainMachine Component Setup'
DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN)
PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN)
CONF_PROGRAM_ID = 'program_id'
CONF_ZONE_ID = 'zone_id'
CONF_ZONE_RUN_TIME = 'zone_run_time'
DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC'
DEFAULT_ICON = 'mdi:water'
DEFAULT_PORT = 8080
DEFAULT_SCAN_INTERVAL = timedelta(seconds=60)
DEFAULT_SSL = True
DEFAULT_ZONE_RUN = 60 * 10
TYPE_FREEZE = 'freeze'
TYPE_FREEZE_PROTECTION = 'freeze_protection'
TYPE_FREEZE_TEMP = 'freeze_protect_temp'
TYPE_HOT_DAYS = 'extra_water_on_hot_days'
TYPE_HOURLY = 'hourly'
TYPE_MONTH = 'month'
TYPE_RAINDELAY = 'raindelay'
TYPE_RAINSENSOR = 'rainsensor'
TYPE_WEEKDAY = 'weekday'
BINARY_SENSORS = {
TYPE_FREEZE: ('Freeze Restrictions', 'mdi:cancel'),
TYPE_FREEZE_PROTECTION: ('Freeze Protection', 'mdi:weather-snowy'),
TYPE_HOT_DAYS: ('Extra Water on Hot Days', 'mdi:thermometer-lines'),
TYPE_HOURLY: ('Hourly Restrictions', 'mdi:cancel'),
TYPE_MONTH: ('Month Restrictions', 'mdi:cancel'),
TYPE_RAINDELAY: ('Rain Delay Restrictions', 'mdi:cancel'),
TYPE_RAINSENSOR: ('Rain Sensor Restrictions', 'mdi:cancel'),
TYPE_WEEKDAY: ('Weekday Restrictions', 'mdi:cancel'),
}
SENSORS = {
TYPE_FREEZE_TEMP: ('Freeze Protect Temperature', 'mdi:thermometer', '°C'),
}
BINARY_SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)):
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)])
})
SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)):
vol.All(cv.ensure_list, [vol.In(SENSORS)])
})
SERVICE_START_PROGRAM_SCHEMA = vol.Schema({
vol.Required(CONF_PROGRAM_ID): cv.positive_int,
})
SERVICE_START_ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE_ID): cv.positive_int,
vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN):
cv.positive_int,
})
SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema({
vol.Required(CONF_PROGRAM_ID): cv.positive_int,
})
SERVICE_STOP_ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE_ID): cv.positive_int,
})
SWITCH_SCHEMA = vol.Schema({vol.Optional(CONF_ZONE_RUN_TIME): cv.positive_int})
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN:
vol.Schema({
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_BINARY_SENSORS, default={}):
BINARY_SENSOR_SCHEMA,
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA,
})
},
extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the RainMachine component."""
from regenmaschine import Authenticator, Client
from regenmaschine.exceptions import RainMachineError
conf = config[DOMAIN]
ip_address = conf[CONF_IP_ADDRESS]
password = conf[CONF_PASSWORD]
port = conf[CONF_PORT]
ssl = conf[CONF_SSL]
try:
auth = Authenticator.create_local(
ip_address, password, port=port, https=ssl)
rainmachine = RainMachine(hass, Client(auth))
rainmachine.update()
hass.data[DATA_RAINMACHINE] = rainmachine
except RainMachineError as exc:
_LOGGER.error('An error occurred: %s', str(exc))
hass.components.persistent_notification.create(
'Error: {0}<br />'
'You will need to restart hass after fixing.'
''.format(exc),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
for component, schema in [
('binary_sensor', conf[CONF_BINARY_SENSORS]),
('sensor', conf[CONF_SENSORS]),
('switch', conf[CONF_SWITCHES]),
]:
discovery.load_platform(hass, component, DOMAIN, schema, config)
def refresh(event_time):
"""Refresh RainMachine data."""
_LOGGER.debug('Updating RainMachine data')
hass.data[DATA_RAINMACHINE].update()
dispatcher_send(hass, DATA_UPDATE_TOPIC)
track_time_interval(hass, refresh, DEFAULT_SCAN_INTERVAL)
def start_program(service):
"""Start a particular program."""
rainmachine.client.programs.start(service.data[CONF_PROGRAM_ID])
def start_zone(service):
"""Start a particular zone for a certain amount of time."""
rainmachine.client.zones.start(service.data[CONF_ZONE_ID],
service.data[CONF_ZONE_RUN_TIME])
def stop_all(service):
"""Stop all watering."""
rainmachine.client.watering.stop_all()
def stop_program(service):
"""Stop a program."""
rainmachine.client.programs.stop(service.data[CONF_PROGRAM_ID])
def stop_zone(service):
"""Stop a zone."""
rainmachine.client.zones.stop(service.data[CONF_ZONE_ID])
for service, method, schema in [
('start_program', start_program, SERVICE_START_PROGRAM_SCHEMA),
('start_zone', start_zone, SERVICE_START_ZONE_SCHEMA),
('stop_all', stop_all, {}),
('stop_program', stop_program, SERVICE_STOP_PROGRAM_SCHEMA),
('stop_zone', stop_zone, SERVICE_STOP_ZONE_SCHEMA)
]:
hass.services.register(DOMAIN, service, method, schema=schema)
return True
class RainMachine(object):
"""Define a generic RainMachine object."""
def __init__(self, hass, client):
"""Initialize."""
self.client = client
self.device_mac = self.client.provision.wifi()['macAddress']
self.restrictions = {}
def update(self):
"""Update sensor/binary sensor data."""
self.restrictions.update({
'current': self.client.restrictions.current(),
'global': self.client.restrictions.universal()
})
class RainMachineEntity(Entity):
"""Define a generic RainMachine entity."""
def __init__(self, rainmachine):
"""Initialize."""
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
self._name = None
self.rainmachine = rainmachine
@property
def device_state_attributes(self) -> dict:
"""Return the state attributes."""
return self._attrs
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name

View File

@ -0,0 +1,32 @@
# Describes the format for available RainMachine services
---
start_program:
description: Start a program.
fields:
program_id:
description: The program to start.
example: 3
start_zone:
description: Start a zone for a set number of seconds.
fields:
zone_id:
description: The zone to start.
example: 3
zone_run_time:
description: The number of seconds to run the zone.
example: 120
stop_all:
description: Stop all watering activities.
stop_program:
description: Stop a program.
fields:
program_id:
description: The program to stop.
example: 3
stop_zone:
description: Stop a zone.
fields:
zone_id:
description: The zone to stop.
example: 3

View File

@ -35,7 +35,7 @@ from . import migration, purge
from .const import DATA_INSTANCE from .const import DATA_INSTANCE
from .util import session_scope from .util import session_scope
REQUIREMENTS = ['sqlalchemy==1.2.7'] REQUIREMENTS = ['sqlalchemy==1.2.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['blockchain==1.4.0'] REQUIREMENTS = ['blockchain==1.4.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -28,6 +28,12 @@ import homeassistant.helpers.config_validation as cv
_RESOURCE = 'http://www.bom.gov.au/fwo/{}/{}.{}.json' _RESOURCE = 'http://www.bom.gov.au/fwo/{}/{}.{}.json'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_LAST_UPDATE = 'last_update'
ATTR_SENSOR_ID = 'sensor_id'
ATTR_STATION_ID = 'station_id'
ATTR_STATION_NAME = 'station_name'
ATTR_ZONE_ID = 'zone_id'
CONF_ATTRIBUTION = "Data provided by the Australian Bureau of Meteorology" CONF_ATTRIBUTION = "Data provided by the Australian Bureau of Meteorology"
CONF_STATION = 'station' CONF_STATION = 'station'
CONF_ZONE_ID = 'zone_id' CONF_ZONE_ID = 'zone_id'
@ -35,7 +41,6 @@ CONF_WMO_ID = 'wmo_id'
MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=35) MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=35)
# Sensor types are defined like: Name, units
SENSOR_TYPES = { SENSOR_TYPES = {
'wmo': ['wmo', None], 'wmo': ['wmo', None],
'name': ['Station Name', None], 'name': ['Station Name', None],
@ -70,7 +75,7 @@ SENSOR_TYPES = {
'weather': ['Weather', None], 'weather': ['Weather', None],
'wind_dir': ['Wind Direction', None], 'wind_dir': ['Wind Direction', None],
'wind_spd_kmh': ['Wind Speed kmh', 'km/h'], 'wind_spd_kmh': ['Wind Speed kmh', 'km/h'],
'wind_spd_kt': ['Wind Direction kt', 'kt'] 'wind_spd_kt': ['Wind Speed kt', 'kt']
} }
@ -98,6 +103,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the BOM sensor.""" """Set up the BOM sensor."""
station = config.get(CONF_STATION) station = config.get(CONF_STATION)
zone_id, wmo_id = config.get(CONF_ZONE_ID), config.get(CONF_WMO_ID) zone_id, wmo_id = config.get(CONF_ZONE_ID), config.get(CONF_WMO_ID)
if station is not None: if station is not None:
if zone_id and wmo_id: if zone_id and wmo_id:
_LOGGER.warning( _LOGGER.warning(
@ -111,17 +117,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hass.config.config_dir) hass.config.config_dir)
if station is None: if station is None:
_LOGGER.error("Could not get BOM weather station from lat/lon") _LOGGER.error("Could not get BOM weather station from lat/lon")
return False return
bom_data = BOMCurrentData(hass, station) bom_data = BOMCurrentData(hass, station)
try: try:
bom_data.update() bom_data.update()
except ValueError as err: except ValueError as err:
_LOGGER.error("Received error from BOM_Current: %s", err) _LOGGER.error("Received error from BOM Current: %s", err)
return False return
add_devices([BOMCurrentSensor(bom_data, variable, config.get(CONF_NAME)) add_devices([BOMCurrentSensor(bom_data, variable, config.get(CONF_NAME))
for variable in config[CONF_MONITORED_CONDITIONS]]) for variable in config[CONF_MONITORED_CONDITIONS]])
return True
class BOMCurrentSensor(Entity): class BOMCurrentSensor(Entity):
@ -150,14 +157,17 @@ class BOMCurrentSensor(Entity):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
attr = {} attr = {
attr['Sensor Id'] = self._condition ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
attr['Zone Id'] = self.bom_data.latest_data['history_product'] ATTR_LAST_UPDATE: datetime.datetime.strptime(
attr['Station Id'] = self.bom_data.latest_data['wmo'] str(self.bom_data.latest_data['local_date_time_full']),
attr['Station Name'] = self.bom_data.latest_data['name'] '%Y%m%d%H%M%S'),
attr['Last Update'] = datetime.datetime.strptime(str( ATTR_SENSOR_ID: self._condition,
self.bom_data.latest_data['local_date_time_full']), '%Y%m%d%H%M%S') ATTR_STATION_ID: self.bom_data.latest_data['wmo'],
attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION ATTR_STATION_NAME: self.bom_data.latest_data['name'],
ATTR_ZONE_ID: self.bom_data.latest_data['history_product'],
}
return attr return attr
@property @property
@ -180,8 +190,9 @@ class BOMCurrentData(object):
self._data = None self._data = None
def _build_url(self): def _build_url(self):
"""Build the URL for the requests."""
url = _RESOURCE.format(self._zone_id, self._zone_id, self._wmo_id) url = _RESOURCE.format(self._zone_id, self._zone_id, self._wmo_id)
_LOGGER.info("BOM URL %s", url) _LOGGER.debug("BOM URL: %s", url)
return url return url
@property @property
@ -200,7 +211,7 @@ class BOMCurrentData(object):
for the latest value that is not `-`. for the latest value that is not `-`.
Iterators are used in this method to avoid iterating needlessly Iterators are used in this method to avoid iterating needlessly
iterating through the entire BOM provided dataset iterating through the entire BOM provided dataset.
""" """
condition_readings = (entry[condition] for entry in self._data) condition_readings = (entry[condition] for entry in self._data)
return next((x for x in condition_readings if x != '-'), None) return next((x for x in condition_readings if x != '-'), None)
@ -257,7 +268,7 @@ def _get_bom_stations():
def bom_stations(cache_dir): def bom_stations(cache_dir):
"""Return {CONF_STATION: (lat, lon)} for all stations, for auto-config. """Return {CONF_STATION: (lat, lon)} for all stations, for auto-config.
Results from internet requests are cached as compressed json, making Results from internet requests are cached as compressed JSON, making
subsequent calls very much faster. subsequent calls very much faster.
""" """
cache_file = os.path.join(cache_dir, '.bom-stations.json.gz') cache_file = os.path.join(cache_dir, '.bom-stations.json.gz')
@ -277,7 +288,7 @@ def closest_station(lat, lon, cache_dir):
stations = bom_stations(cache_dir) stations = bom_stations(cache_dir)
def comparable_dist(wmo_id): def comparable_dist(wmo_id):
"""Create a psudeo-distance from lat/lon.""" """Create a psudeo-distance from latitude/longitude."""
station_lat, station_lon = stations[wmo_id] station_lat, station_lon = stations[wmo_id]
return (lat - station_lat) ** 2 + (lon - station_lon) ** 2 return (lat - station_lat) ** 2 + (lon - station_lon) ** 2

View File

@ -13,64 +13,78 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_CURRENCY) ATTR_ATTRIBUTION, CONF_DISPLAY_CURRENCY)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['coinmarketcap==4.2.1'] REQUIREMENTS = ['coinmarketcap==5.0.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_24H_VOLUME = '24h_volume' ATTR_VOLUME_24H = 'volume_24h'
ATTR_AVAILABLE_SUPPLY = 'available_supply' ATTR_AVAILABLE_SUPPLY = 'available_supply'
ATTR_CIRCULATING_SUPPLY = 'circulating_supply'
ATTR_MARKET_CAP = 'market_cap' ATTR_MARKET_CAP = 'market_cap'
ATTR_PERCENT_CHANGE_24H = 'percent_change_24h' ATTR_PERCENT_CHANGE_24H = 'percent_change_24h'
ATTR_PERCENT_CHANGE_7D = 'percent_change_7d' ATTR_PERCENT_CHANGE_7D = 'percent_change_7d'
ATTR_PERCENT_CHANGE_1H = 'percent_change_1h' ATTR_PERCENT_CHANGE_1H = 'percent_change_1h'
ATTR_PRICE = 'price' ATTR_PRICE = 'price'
ATTR_RANK = 'rank'
ATTR_SYMBOL = 'symbol' ATTR_SYMBOL = 'symbol'
ATTR_TOTAL_SUPPLY = 'total_supply' ATTR_TOTAL_SUPPLY = 'total_supply'
CONF_ATTRIBUTION = "Data provided by CoinMarketCap" CONF_ATTRIBUTION = "Data provided by CoinMarketCap"
CONF_CURRENCY_ID = 'currency_id'
CONF_DISPLAY_CURRENCY_DECIMALS = 'display_currency_decimals'
DEFAULT_CURRENCY = 'bitcoin' DEFAULT_CURRENCY_ID = 1
DEFAULT_DISPLAY_CURRENCY = 'USD' DEFAULT_DISPLAY_CURRENCY = 'USD'
DEFAULT_DISPLAY_CURRENCY_DECIMALS = 2
ICON = 'mdi:currency-usd' ICON = 'mdi:currency-usd'
SCAN_INTERVAL = timedelta(minutes=15) SCAN_INTERVAL = timedelta(minutes=15)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_CURRENCY, default=DEFAULT_CURRENCY): cv.string, vol.Optional(CONF_CURRENCY_ID, default=DEFAULT_CURRENCY_ID):
cv.positive_int,
vol.Optional(CONF_DISPLAY_CURRENCY, default=DEFAULT_DISPLAY_CURRENCY): vol.Optional(CONF_DISPLAY_CURRENCY, default=DEFAULT_DISPLAY_CURRENCY):
cv.string, cv.string,
vol.Optional(CONF_DISPLAY_CURRENCY_DECIMALS,
default=DEFAULT_DISPLAY_CURRENCY_DECIMALS):
vol.All(vol.Coerce(int), vol.Range(min=1)),
}) })
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the CoinMarketCap sensor.""" """Set up the CoinMarketCap sensor."""
currency = config.get(CONF_CURRENCY) currency_id = config.get(CONF_CURRENCY_ID)
display_currency = config.get(CONF_DISPLAY_CURRENCY).lower() display_currency = config.get(CONF_DISPLAY_CURRENCY).upper()
display_currency_decimals = config.get(CONF_DISPLAY_CURRENCY_DECIMALS)
try: try:
CoinMarketCapData(currency, display_currency).update() CoinMarketCapData(currency_id, display_currency).update()
except HTTPError: except HTTPError:
_LOGGER.warning("Currency %s or display currency %s is not available. " _LOGGER.warning("Currency ID %s or display currency %s "
"Using bitcoin and USD.", currency, display_currency) "is not available. Using 1 (bitcoin) "
currency = DEFAULT_CURRENCY "and USD.", currency_id, display_currency)
currency_id = DEFAULT_CURRENCY_ID
display_currency = DEFAULT_DISPLAY_CURRENCY display_currency = DEFAULT_DISPLAY_CURRENCY
add_devices([CoinMarketCapSensor( add_devices([CoinMarketCapSensor(
CoinMarketCapData(currency, display_currency))], True) CoinMarketCapData(
currency_id, display_currency), display_currency_decimals)], True)
class CoinMarketCapSensor(Entity): class CoinMarketCapSensor(Entity):
"""Representation of a CoinMarketCap sensor.""" """Representation of a CoinMarketCap sensor."""
def __init__(self, data): def __init__(self, data, display_currency_decimals):
"""Initialize the sensor.""" """Initialize the sensor."""
self.data = data self.data = data
self.display_currency_decimals = display_currency_decimals
self._ticker = None self._ticker = None
self._unit_of_measurement = self.data.display_currency.upper() self._unit_of_measurement = self.data.display_currency
@property @property
def name(self): def name(self):
@ -80,8 +94,9 @@ class CoinMarketCapSensor(Entity):
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return round(float(self._ticker.get( return round(float(
'price_{}'.format(self.data.display_currency))), 2) self._ticker.get('quotes').get(self.data.display_currency)
.get('price')), self.display_currency_decimals)
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
@ -97,15 +112,24 @@ class CoinMarketCapSensor(Entity):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the sensor.""" """Return the state attributes of the sensor."""
return { return {
ATTR_24H_VOLUME: self._ticker.get( ATTR_VOLUME_24H:
'24h_volume_{}'.format(self.data.display_currency)), self._ticker.get('quotes').get(self.data.display_currency)
.get('volume_24h'),
ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
ATTR_AVAILABLE_SUPPLY: self._ticker.get('available_supply'), ATTR_CIRCULATING_SUPPLY: self._ticker.get('circulating_supply'),
ATTR_MARKET_CAP: self._ticker.get( ATTR_MARKET_CAP:
'market_cap_{}'.format(self.data.display_currency)), self._ticker.get('quotes').get(self.data.display_currency)
ATTR_PERCENT_CHANGE_24H: self._ticker.get('percent_change_24h'), .get('market_cap'),
ATTR_PERCENT_CHANGE_7D: self._ticker.get('percent_change_7d'), ATTR_PERCENT_CHANGE_24H:
ATTR_PERCENT_CHANGE_1H: self._ticker.get('percent_change_1h'), self._ticker.get('quotes').get(self.data.display_currency)
.get('percent_change_24h'),
ATTR_PERCENT_CHANGE_7D:
self._ticker.get('quotes').get(self.data.display_currency)
.get('percent_change_7d'),
ATTR_PERCENT_CHANGE_1H:
self._ticker.get('quotes').get(self.data.display_currency)
.get('percent_change_1h'),
ATTR_RANK: self._ticker.get('rank'),
ATTR_SYMBOL: self._ticker.get('symbol'), ATTR_SYMBOL: self._ticker.get('symbol'),
ATTR_TOTAL_SUPPLY: self._ticker.get('total_supply'), ATTR_TOTAL_SUPPLY: self._ticker.get('total_supply'),
} }
@ -113,20 +137,20 @@ class CoinMarketCapSensor(Entity):
def update(self): def update(self):
"""Get the latest data and updates the states.""" """Get the latest data and updates the states."""
self.data.update() self.data.update()
self._ticker = self.data.ticker[0] self._ticker = self.data.ticker.get('data')
class CoinMarketCapData(object): class CoinMarketCapData(object):
"""Get the latest data and update the states.""" """Get the latest data and update the states."""
def __init__(self, currency, display_currency): def __init__(self, currency_id, display_currency):
"""Initialize the data object.""" """Initialize the data object."""
self.currency = currency self.currency_id = currency_id
self.display_currency = display_currency self.display_currency = display_currency
self.ticker = None self.ticker = None
def update(self): def update(self):
"""Get the latest data from blockchain.info.""" """Get the latest data from coinmarketcap.com."""
from coinmarketcap import Market from coinmarketcap import Market
self.ticker = Market().ticker( self.ticker = Market().ticker(
self.currency, limit=1, convert=self.display_currency) self.currency_id, convert=self.display_currency)

View File

@ -5,7 +5,8 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/sensor.deconz/ https://home-assistant.io/components/sensor.deconz/
""" """
from homeassistant.components.deconz import ( from homeassistant.components.deconz import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID,
DATA_DECONZ_UNSUB)
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
from homeassistant.core import callback from homeassistant.core import callback
@ -33,14 +34,17 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
"""Add sensors from deCONZ.""" """Add sensors from deCONZ."""
from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE
entities = [] entities = []
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors: for sensor in sensors:
if sensor.type in DECONZ_SENSOR: if sensor.type in DECONZ_SENSOR and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
if sensor.type in DECONZ_REMOTE: if sensor.type in DECONZ_REMOTE:
if sensor.battery: if sensor.battery:
entities.append(DeconzBattery(sensor)) entities.append(DeconzBattery(sensor))
else: else:
entities.append(DeconzSensor(sensor)) entities.append(DeconzSensor(sensor))
async_add_devices(entities, True) async_add_devices(entities, True)
hass.data[DATA_DECONZ_UNSUB].append( hass.data[DATA_DECONZ_UNSUB].append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
@ -114,9 +118,12 @@ class DeconzSensor(Entity):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the sensor.""" """Return the state attributes of the sensor."""
from pydeconz.sensor import LIGHTLEVEL
attr = {} attr = {}
if self._sensor.battery: if self._sensor.battery:
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
if self._sensor.type in LIGHTLEVEL and self._sensor.dark is not None:
attr['dark'] = self._sensor.dark
if self.unit_of_measurement == 'Watts': if self.unit_of_measurement == 'Watts':
attr[ATTR_CURRENT] = self._sensor.current attr[ATTR_CURRENT] = self._sensor.current
attr[ATTR_VOLTAGE] = self._sensor.voltage attr[ATTR_VOLTAGE] = self._sensor.voltage

View File

@ -9,7 +9,6 @@ https://home-assistant.io/components/sensor.ebox/
import logging import logging
from datetime import timedelta from datetime import timedelta
import requests
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -18,9 +17,11 @@ from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_USERNAME, CONF_PASSWORD,
CONF_NAME, CONF_MONITORED_VARIABLES) CONF_NAME, CONF_MONITORED_VARIABLES)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.exceptions import PlatformNotReady
# pylint: disable=import-error
REQUIREMENTS = [] # ['pyebox==0.1.0'] - disabled because it breaks pip10 REQUIREMENTS = ['pyebox==1.1.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,7 +33,8 @@ PERCENT = '%' # type: str
DEFAULT_NAME = 'EBox' DEFAULT_NAME = 'EBox'
REQUESTS_TIMEOUT = 15 REQUESTS_TIMEOUT = 15
SCAN_INTERVAL = timedelta(minutes=5) SCAN_INTERVAL = timedelta(minutes=15)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
SENSOR_TYPES = { SENSOR_TYPES = {
'usage': ['Usage', PERCENT, 'mdi:percent'], 'usage': ['Usage', PERCENT, 'mdi:percent'],
@ -62,25 +64,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
def setup_platform(hass, config, add_devices, discovery_info=None): async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the EBox sensor.""" """Set up the EBox sensor."""
username = config.get(CONF_USERNAME) username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD) password = config.get(CONF_PASSWORD)
try: httpsession = hass.helpers.aiohttp_client.async_get_clientsession()
ebox_data = EBoxData(username, password) ebox_data = EBoxData(username, password, httpsession)
ebox_data.update()
except requests.exceptions.HTTPError as error:
_LOGGER.error("Failed login: %s", error)
return False
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
from pyebox.client import PyEboxError
try:
await ebox_data.async_update()
except PyEboxError as exp:
_LOGGER.error("Failed login: %s", exp)
raise PlatformNotReady
sensors = [] sensors = []
for variable in config[CONF_MONITORED_VARIABLES]: for variable in config[CONF_MONITORED_VARIABLES]:
sensors.append(EBoxSensor(ebox_data, variable, name)) sensors.append(EBoxSensor(ebox_data, variable, name))
add_devices(sensors, True) async_add_devices(sensors, True)
class EBoxSensor(Entity): class EBoxSensor(Entity):
@ -116,9 +122,9 @@ class EBoxSensor(Entity):
"""Icon to use in the frontend, if any.""" """Icon to use in the frontend, if any."""
return self._icon return self._icon
def update(self): async def async_update(self):
"""Get the latest data from EBox and update the state.""" """Get the latest data from EBox and update the state."""
self.ebox_data.update() await self.ebox_data.async_update()
if self.type in self.ebox_data.data: if self.type in self.ebox_data.data:
self._state = round(self.ebox_data.data[self.type], 2) self._state = round(self.ebox_data.data[self.type], 2)
@ -126,18 +132,21 @@ class EBoxSensor(Entity):
class EBoxData(object): class EBoxData(object):
"""Get data from Ebox.""" """Get data from Ebox."""
def __init__(self, username, password): def __init__(self, username, password, httpsession):
"""Initialize the data object.""" """Initialize the data object."""
from pyebox import EboxClient from pyebox import EboxClient
self.client = EboxClient(username, password, REQUESTS_TIMEOUT) self.client = EboxClient(username, password,
REQUESTS_TIMEOUT, httpsession)
self.data = {} self.data = {}
def update(self): @Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
"""Get the latest data from Ebox.""" """Get the latest data from Ebox."""
from pyebox.client import PyEboxError from pyebox.client import PyEboxError
try: try:
self.client.fetch_data() await self.client.fetch_data()
except PyEboxError as exp: except PyEboxError as exp:
_LOGGER.error("Error on receive last EBox data: %s", exp) _LOGGER.error("Error on receive last EBox data: %s", exp)
return return
# Update data
self.data = self.client.get_data() self.data = self.client.get_data()

View File

@ -8,12 +8,12 @@ import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, CONF_API_KEY, CONF_ROOM from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_ROOM
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['gitterpy==0.1.6'] REQUIREMENTS = ['gitterpy==0.1.7']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
username = gitter.auth.get_my_id['name'] username = gitter.auth.get_my_id['name']
except GitterTokenError: except GitterTokenError:
_LOGGER.error("Token is not valid") _LOGGER.error("Token is not valid")
return False return
add_devices([GitterSensor(gitter, room, name, username)], True) add_devices([GitterSensor(gitter, room, name, username)], True)
@ -96,7 +96,14 @@ class GitterSensor(Entity):
def update(self): def update(self):
"""Get the latest data and updates the state.""" """Get the latest data and updates the state."""
from gitterpy.errors import GitterRoomError
try:
data = self._data.user.unread_items(self._room) data = self._data.user.unread_items(self._room)
except GitterRoomError as error:
_LOGGER.error(error)
return
if 'error' not in data.keys(): if 'error' not in data.keys():
self._mention = len(data['mention']) self._mention = len(data['mention'])
self._state = len(data['chat']) self._state = len(data['chat'])

View File

@ -161,7 +161,8 @@ class GlancesSensor(Entity):
elif self.type == 'docker_active': elif self.type == 'docker_active':
count = 0 count = 0
for container in value['docker']['containers']: for container in value['docker']['containers']:
if container['Status'] == 'running': if container['Status'] == 'running' or \
'Up' in container['Status']:
count += 1 count += 1
self._state = count self._state = count
elif self.type == 'docker_cpu_use': elif self.type == 'docker_cpu_use':

View File

@ -10,7 +10,9 @@ import logging
from homeassistant.components.homematicip_cloud import ( from homeassistant.components.homematicip_cloud import (
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
ATTR_HOME_ID) ATTR_HOME_ID)
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import (
TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_devices,
"""Set up the HomematicIP sensors devices.""" """Set up the HomematicIP sensors devices."""
from homematicip.device import ( from homematicip.device import (
HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, HeatingThermostat, TemperatureHumiditySensorWithoutDisplay,
TemperatureHumiditySensorDisplay) TemperatureHumiditySensorDisplay, MotionDetectorIndoor)
if discovery_info is None: if discovery_info is None:
return return
@ -50,6 +52,8 @@ async def async_setup_platform(hass, config, async_add_devices,
TemperatureHumiditySensorWithoutDisplay)): TemperatureHumiditySensorWithoutDisplay)):
devices.append(HomematicipTemperatureSensor(home, device)) devices.append(HomematicipTemperatureSensor(home, device))
devices.append(HomematicipHumiditySensor(home, device)) devices.append(HomematicipHumiditySensor(home, device))
if isinstance(device, MotionDetectorIndoor):
devices.append(HomematicipIlluminanceSensor(home, device))
if devices: if devices:
async_add_devices(devices) async_add_devices(devices)
@ -149,6 +153,11 @@ class HomematicipHumiditySensor(HomematicipGenericDevice):
"""Initialize the thermometer device.""" """Initialize the thermometer device."""
super().__init__(home, device, 'Humidity') super().__init__(home, device, 'Humidity')
@property
def device_class(self):
"""Return the device class of the sensor."""
return DEVICE_CLASS_HUMIDITY
@property @property
def icon(self): def icon(self):
"""Return the icon.""" """Return the icon."""
@ -172,6 +181,11 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice):
"""Initialize the thermometer device.""" """Initialize the thermometer device."""
super().__init__(home, device, 'Temperature') super().__init__(home, device, 'Temperature')
@property
def device_class(self):
"""Return the device class of the sensor."""
return DEVICE_CLASS_TEMPERATURE
@property @property
def icon(self): def icon(self):
"""Return the icon.""" """Return the icon."""
@ -186,3 +200,26 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice):
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit this state is expressed in.""" """Return the unit this state is expressed in."""
return TEMP_CELSIUS return TEMP_CELSIUS
class HomematicipIlluminanceSensor(HomematicipGenericDevice):
"""MomematicIP the thermometer device."""
def __init__(self, home, device):
"""Initialize the device."""
super().__init__(home, device, 'Illuminance')
@property
def device_class(self):
"""Return the device class of the sensor."""
return DEVICE_CLASS_ILLUMINANCE
@property
def state(self):
"""Return the state."""
return self._device.illumination
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return 'lx'

View File

@ -0,0 +1,72 @@
"""
Support for Hydrawise sprinkler.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.hydrawise/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.hydrawise import (
DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP, DEVICE_MAP_INDEX, SENSORS)
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_MONITORED_CONDITIONS
DEPENDENCIES = ['hydrawise']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSORS):
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a sensor for a Hydrawise device."""
hydrawise = hass.data[DATA_HYDRAWISE].data
sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
for zone in hydrawise.relays:
sensors.append(HydrawiseSensor(zone, sensor_type))
add_devices(sensors, True)
class HydrawiseSensor(HydrawiseEntity):
"""A sensor implementation for Hydrawise device."""
@property
def state(self):
"""Return the state of the sensor."""
return self._state
def update(self):
"""Get the latest data and updates the states."""
mydata = self.hass.data[DATA_HYDRAWISE].data
_LOGGER.debug("Updating Hydrawise sensor: %s", self._name)
if self._sensor_type == 'watering_time':
if not mydata.running:
self._state = 0
else:
if int(mydata.running[0]['relay']) == self.data['relay']:
self._state = int(mydata.running[0]['time_left']/60)
else:
self._state = 0
else: # _sensor_type == 'next_cycle'
for relay in mydata.relays:
if relay['relay'] == self.data['relay']:
if relay['nicetime'] == 'Not scheduled':
self._state = 'not_scheduled'
else:
self._state = relay['nicetime'].split(',')[0] + \
' ' + relay['nicetime'].split(' ')[3]
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return DEVICE_MAP[self._sensor_type][
DEVICE_MAP_INDEX.index('ICON_INDEX')]

View File

@ -0,0 +1,195 @@
"""
Support for Iperf3 network measurement tool.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.iperf3/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA
from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ENTITY_ID, CONF_MONITORED_CONDITIONS,
CONF_HOST, CONF_PORT, CONF_PROTOCOL)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['iperf3==0.1.10']
_LOGGER = logging.getLogger(__name__)
ATTR_PROTOCOL = 'Protocol'
ATTR_REMOTE_HOST = 'Remote Server'
ATTR_REMOTE_PORT = 'Remote Port'
ATTR_VERSION = 'Version'
CONF_ATTRIBUTION = 'Data retrieved using Iperf3'
CONF_DURATION = 'duration'
CONF_PARALLEL = 'parallel'
DEFAULT_DURATION = 10
DEFAULT_PORT = 5201
DEFAULT_PARALLEL = 1
DEFAULT_PROTOCOL = 'tcp'
IPERF3_DATA = 'iperf3'
SCAN_INTERVAL = timedelta(minutes=60)
SERVICE_NAME = 'iperf3_update'
ICON = 'mdi:speedometer'
SENSOR_TYPES = {
'download': ['Download', 'Mbit/s'],
'upload': ['Upload', 'Mbit/s'],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]),
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Range(5, 10),
vol.Optional(CONF_PARALLEL, default=DEFAULT_PARALLEL): vol.Range(1, 20),
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL):
vol.In(['tcp', 'udp']),
})
SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Iperf3 sensor."""
if hass.data.get(IPERF3_DATA) is None:
hass.data[IPERF3_DATA] = {}
hass.data[IPERF3_DATA]['sensors'] = []
dev = []
for sensor in config[CONF_MONITORED_CONDITIONS]:
dev.append(
Iperf3Sensor(config[CONF_HOST],
config[CONF_PORT],
config[CONF_DURATION],
config[CONF_PARALLEL],
config[CONF_PROTOCOL],
sensor))
hass.data[IPERF3_DATA]['sensors'].extend(dev)
add_devices(dev)
def _service_handler(service):
"""Update service for manual updates."""
entity_id = service.data.get('entity_id')
all_iperf3_sensors = hass.data[IPERF3_DATA]['sensors']
for sensor in all_iperf3_sensors:
if entity_id is not None:
if sensor.entity_id == entity_id:
sensor.update()
sensor.schedule_update_ha_state()
break
else:
sensor.update()
sensor.schedule_update_ha_state()
for sensor in dev:
hass.services.register(DOMAIN, SERVICE_NAME, _service_handler,
schema=SERVICE_SCHEMA)
class Iperf3Sensor(Entity):
"""A Iperf3 sensor implementation."""
def __init__(self, server, port, duration, streams,
protocol, sensor_type):
"""Initialize the sensor."""
self._attrs = {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
ATTR_PROTOCOL: protocol,
}
self._name = \
"{} {}".format(SENSOR_TYPES[sensor_type][0], server)
self._state = None
self._sensor_type = sensor_type
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self._port = port
self._server = server
self._duration = duration
self._num_streams = streams
self._protocol = protocol
self.result = None
@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._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self.result is not None:
self._attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
self._attrs[ATTR_REMOTE_HOST] = self.result.remote_host
self._attrs[ATTR_REMOTE_PORT] = self.result.remote_port
self._attrs[ATTR_VERSION] = self.result.version
return self._attrs
def update(self):
"""Get the latest data and update the states."""
import iperf3
client = iperf3.Client()
client.duration = self._duration
client.server_hostname = self._server
client.port = self._port
client.verbose = False
client.num_streams = self._num_streams
client.protocol = self._protocol
# when testing download bandwith, reverse must be True
if self._sensor_type == 'download':
client.reverse = True
try:
self.result = client.run()
except (AttributeError, OSError, ValueError) as error:
self.result = None
_LOGGER.error("Iperf3 sensor error: %s", error)
return
if self.result is not None and \
hasattr(self.result, 'error') and \
self.result.error is not None:
_LOGGER.error("Iperf3 sensor error: %s", self.result.error)
self.result = None
return
# UDP only have 1 way attribute
if self._protocol == 'udp':
self._state = round(self.result.Mbps, 2)
elif self._sensor_type == 'download':
self._state = round(self.result.received_Mbps, 2)
elif self._sensor_type == 'upload':
self._state = round(self.result.sent_Mbps, 2)
@property
def icon(self):
"""Return icon."""
return ICON

View File

@ -4,7 +4,6 @@ Support for Luftdaten sensors.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.luftdaten/ https://home-assistant.io/components/sensor.luftdaten/
""" """
import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
@ -19,7 +18,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['luftdaten==0.1.3'] REQUIREMENTS = ['luftdaten==0.2.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -59,8 +58,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine async def async_setup_platform(
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): hass, config, async_add_devices, discovery_info=None):
"""Set up the Luftdaten sensor.""" """Set up the Luftdaten sensor."""
from luftdaten import Luftdaten from luftdaten import Luftdaten
@ -71,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
luftdaten = LuftdatenData(Luftdaten(sensor_id, hass.loop, session)) luftdaten = LuftdatenData(Luftdaten(sensor_id, hass.loop, session))
yield from luftdaten.async_update() await luftdaten.async_update()
if luftdaten.data is None: if luftdaten.data is None:
_LOGGER.error("Sensor is not available: %s", sensor_id) _LOGGER.error("Sensor is not available: %s", sensor_id)

View File

@ -33,6 +33,7 @@ DIGITS = {
SENSOR_MODELS = [ SENSOR_MODELS = [
'Ubiquiti mFi-THS', 'Ubiquiti mFi-THS',
'Ubiquiti mFi-CS', 'Ubiquiti mFi-CS',
'Ubiquiti mFi-DS',
'Outlet', 'Outlet',
'Input Analog', 'Input Analog',
'Input Digital', 'Input Digital',

View File

@ -4,42 +4,44 @@ Support for Nest Thermostat Sensors.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.nest/ https://home-assistant.io/components/sensor.nest/
""" """
from itertools import chain
import logging import logging
from homeassistant.components.nest import DATA_NEST from homeassistant.components.nest import DATA_NEST, NestSensorDevice
from homeassistant.helpers.entity import Entity
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS,
DEVICE_CLASS_TEMPERATURE) DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY)
DEPENDENCIES = ['nest'] DEPENDENCIES = ['nest']
SENSOR_TYPES = ['humidity',
'operation_mode', SENSOR_TYPES = ['humidity', 'operation_mode', 'hvac_state']
'hvac_state']
TEMP_SENSOR_TYPES = ['temperature', 'target']
PROTECT_SENSOR_TYPES = ['co_status', 'smoke_status', 'battery_health']
STRUCTURE_SENSOR_TYPES = ['eta']
_VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \
+ STRUCTURE_SENSOR_TYPES
SENSOR_UNITS = {'humidity': '%'}
SENSOR_DEVICE_CLASSES = {'humidity': DEVICE_CLASS_HUMIDITY}
VARIABLE_NAME_MAPPING = {'eta': 'eta_begin', 'operation_mode': 'mode'}
SENSOR_TYPES_DEPRECATED = ['last_ip', SENSOR_TYPES_DEPRECATED = ['last_ip',
'local_ip', 'local_ip',
'last_connection'] 'last_connection',
'battery_level']
DEPRECATED_WEATHER_VARS = {'weather_humidity': 'humidity', DEPRECATED_WEATHER_VARS = ['weather_humidity',
'weather_temperature': 'temperature', 'weather_temperature',
'weather_condition': 'condition', 'weather_condition',
'wind_speed': 'kph', 'wind_speed',
'wind_direction': 'direction'} 'wind_direction']
SENSOR_UNITS = {'humidity': '%', 'temperature': '°C'} _SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED + DEPRECATED_WEATHER_VARS
PROTECT_VARS = ['co_status', 'smoke_status', 'battery_health']
PROTECT_VARS_DEPRECATED = ['battery_level']
SENSOR_TEMP_TYPES = ['temperature', 'target']
_SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED \
+ list(DEPRECATED_WEATHER_VARS.keys()) + PROTECT_VARS_DEPRECATED
_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -69,53 +71,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"monitored_conditions. See " "monitored_conditions. See "
"https://home-assistant.io/components/" "https://home-assistant.io/components/"
"binary_sensor.nest/ for valid options.") "binary_sensor.nest/ for valid options.")
_LOGGER.error(wstr) _LOGGER.error(wstr)
all_sensors = [] all_sensors = []
for structure, device in chain(nest.thermostats(), nest.smoke_co_alarms()): for structure in nest.structures():
sensors = [NestBasicSensor(structure, device, variable) all_sensors += [NestBasicSensor(structure, None, variable)
for variable in conditions for variable in conditions
if variable in SENSOR_TYPES and device.is_thermostat] if variable in STRUCTURE_SENSOR_TYPES]
sensors += [NestTempSensor(structure, device, variable)
for structure, device in nest.thermostats():
all_sensors += [NestBasicSensor(structure, device, variable)
for variable in conditions for variable in conditions
if variable in SENSOR_TEMP_TYPES and device.is_thermostat] if variable in SENSOR_TYPES]
sensors += [NestProtectSensor(structure, device, variable) all_sensors += [NestTempSensor(structure, device, variable)
for variable in conditions for variable in conditions
if variable in PROTECT_VARS and device.is_smoke_co_alarm] if variable in TEMP_SENSOR_TYPES]
all_sensors.extend(sensors)
for structure, device in nest.smoke_co_alarms():
all_sensors += [NestBasicSensor(structure, device, variable)
for variable in conditions
if variable in PROTECT_SENSOR_TYPES]
add_devices(all_sensors, True) add_devices(all_sensors, True)
class NestSensor(Entity): class NestBasicSensor(NestSensorDevice):
"""Representation of a Nest sensor."""
def __init__(self, structure, device, variable):
"""Initialize the sensor."""
self.structure = structure
self.device = device
self.variable = variable
# device specific
self._location = self.device.where
self._name = "{} {}".format(self.device.name_long,
self.variable.replace("_", " "))
self._state = None
self._unit = None
@property
def name(self):
"""Return the name of the nest, if any."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit
class NestBasicSensor(NestSensor):
"""Representation a basic Nest sensor.""" """Representation a basic Nest sensor."""
@property @property
@ -123,17 +103,26 @@ class NestBasicSensor(NestSensor):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._state return self._state
@property
def device_class(self):
"""Return the device class of the sensor."""
return SENSOR_DEVICE_CLASSES.get(self.variable)
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
self._unit = SENSOR_UNITS.get(self.variable, None) self._unit = SENSOR_UNITS.get(self.variable)
if self.variable == 'operation_mode': if self.variable in VARIABLE_NAME_MAPPING:
self._state = getattr(self.device, "mode") self._state = getattr(self.device,
VARIABLE_NAME_MAPPING[self.variable])
elif self.variable in PROTECT_SENSOR_TYPES:
# keep backward compatibility
self._state = getattr(self.device, self.variable).capitalize()
else: else:
self._state = getattr(self.device, self.variable) self._state = getattr(self.device, self.variable)
class NestTempSensor(NestSensor): class NestTempSensor(NestSensorDevice):
"""Representation of a Nest Temperature sensor.""" """Representation of a Nest Temperature sensor."""
@property @property
@ -162,16 +151,3 @@ class NestTempSensor(NestSensor):
self._state = "%s-%s" % (int(low), int(high)) self._state = "%s-%s" % (int(low), int(high))
else: else:
self._state = round(temp, 1) self._state = round(temp, 1)
class NestProtectSensor(NestSensor):
"""Return the state of nest protect."""
@property
def state(self):
"""Return the state of the sensor."""
return self._state
def update(self):
"""Retrieve latest state."""
self._state = getattr(self.device, self.variable).capitalize()

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