Merge pull request #2323 from home-assistant/dev

0.22
This commit is contained in:
Paulus Schoutsen 2016-06-18 13:20:51 -07:00 committed by GitHub
commit 8c505e625b
103 changed files with 4851 additions and 826 deletions

View File

@ -78,6 +78,9 @@ omit =
homeassistant/components/enocean.py
homeassistant/components/*/enocean.py
homeassistant/components/netatmo.py
homeassistant/components/*/netatmo.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/binary_sensor/arest.py
@ -92,6 +95,7 @@ omit =
homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/bluetooth_tracker.py
homeassistant/components/device_tracker/bt_home_hub_5.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/icloud.py
@ -126,6 +130,7 @@ omit =
homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/pandora.py
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
@ -171,16 +176,18 @@ omit =
homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/netatmo.py
homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nzbget.py
homeassistant/components/sensor/onewire.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/plex.py
homeassistant/components/sensor/rest.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/steam_online.py
homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_hydrological_data.py
homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/temper.py
@ -196,6 +203,7 @@ omit =
homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/mystrom.py
homeassistant/components/switch/netio.py
homeassistant/components/switch/orvibo.py
homeassistant/components/switch/pulseaudio_loopback.py
homeassistant/components/switch/rest.py

567
docs/swagger.yaml Normal file
View File

@ -0,0 +1,567 @@
swagger: '2.0'
info:
title: Home Assistant
description: Home Assistant REST API
version: "1.0.0"
# 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
temperature_unit:
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

@ -9,7 +9,6 @@ import os
import voluptuous as vol
from homeassistant.components import verisure
from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
@ -24,11 +23,6 @@ SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
verisure.DISCOVER_ALARMS: 'verisure'
}
SERVICE_TO_METHOD = {
SERVICE_ALARM_DISARM: 'alarm_disarm',
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
@ -50,8 +44,7 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
def setup(hass, config):
"""Track states and offer events for sensors."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)

View File

@ -9,8 +9,6 @@ import logging
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.const import (STATE_ON, STATE_OFF)
from homeassistant.components import (
bloomsky, mysensors, zwave, vera, wemo, wink)
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
DOMAIN = 'binary_sensor'
@ -35,22 +33,11 @@ SENSOR_CLASSES = [
'vibration', # On means vibration detected, Off means no vibration
]
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
bloomsky.DISCOVER_BINARY_SENSORS: 'bloomsky',
mysensors.DISCOVER_BINARY_SENSORS: 'mysensors',
zwave.DISCOVER_BINARY_SENSORS: 'zwave',
vera.DISCOVER_BINARY_SENSORS: 'vera',
wemo.DISCOVER_BINARY_SENSORS: 'wemo',
wink.DISCOVER_BINARY_SENSORS: 'wink'
}
def setup(hass, config):
"""Track states and offer events for binary sensors."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)

View File

@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
rest.update()
if rest.data is None:
_LOGGER.error('Unable to fetch Rest data')
_LOGGER.error('Unable to fetch REST data')
return False
add_devices([RestBinarySensor(
@ -57,6 +57,7 @@ class RestBinarySensor(BinarySensorDevice):
self._name = name
self._sensor_class = sensor_class
self._state = False
self._previous_data = None
self._value_template = value_template
self.update()
@ -77,9 +78,14 @@ class RestBinarySensor(BinarySensorDevice):
return False
if self._value_template is not None:
self.rest.data = template.render_with_possible_json_value(
response = template.render_with_possible_json_value(
self._hass, self._value_template, self.rest.data, False)
return bool(int(self.rest.data))
try:
return bool(int(response))
except ValueError:
return {"true": True, "on": True, "open": True,
"yes": True}.get(response.lower(), False)
def update(self):
"""Get the latest data from REST API and updates the state."""

View File

@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['python-wink==0.7.6']
REQUIREMENTS = ['python-wink==0.7.7']
# These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = {

View File

@ -9,9 +9,8 @@ from datetime import timedelta
import requests
from homeassistant.components import discovery
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import validate_config
from homeassistant.helpers import validate_config, discovery
from homeassistant.util import Throttle
DOMAIN = "bloomsky"
@ -23,10 +22,6 @@ _LOGGER = logging.getLogger(__name__)
# no point in polling the API more frequently
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300)
DISCOVER_SENSORS = 'bloomsky.sensors'
DISCOVER_BINARY_SENSORS = 'bloomsky.binary_sensor'
DISCOVER_CAMERAS = 'bloomsky.camera'
# pylint: disable=unused-argument,too-few-public-methods
def setup(hass, config):
@ -45,11 +40,8 @@ def setup(hass, config):
except RuntimeError:
return False
for component, discovery_service in (
('camera', DISCOVER_CAMERAS), ('sensor', DISCOVER_SENSORS),
('binary_sensor', DISCOVER_BINARY_SENSORS)):
discovery.discover(hass, discovery_service, component=component,
hass_config=config)
for component in 'camera', 'binary_sensor', 'sensor':
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True

View File

@ -9,7 +9,6 @@ import logging
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import bloomsky
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView
@ -18,11 +17,6 @@ DEPENDENCIES = ['http']
SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
bloomsky.DISCOVER_CAMERAS: 'bloomsky',
}
STATE_RECORDING = 'recording'
STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle'
@ -34,8 +28,7 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
def setup(hass, config):
"""Setup the camera component."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
hass.wsgi.register_view(CameraImageView(hass, component.entities))
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))

View File

@ -49,7 +49,7 @@ class FoscamCamera(Camera):
def camera_image(self):
"""Return a still image reponse from the camera."""
# Send the request to snap a picture and return raw jpg data
response = requests.get(self._snap_picture_url)
response = requests.get(self._snap_picture_url, timeout=10)
return response.content

View File

@ -43,13 +43,14 @@ class GenericCamera(Camera):
try:
response = requests.get(
self._still_image_url,
auth=HTTPBasicAuth(self._username, self._password))
auth=HTTPBasicAuth(self._username, self._password),
timeout=10)
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
else:
try:
response = requests.get(self._still_image_url)
response = requests.get(self._still_image_url, timeout=10)
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None

View File

@ -0,0 +1,53 @@
"""Camera that loads a picture from a local file."""
import logging
import os
from homeassistant.components.camera import Camera
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Camera."""
# check for missing required configuration variable
if config.get("file_path") is None:
_LOGGER.error("Missing required variable: file_path")
return False
setup_config = (
{
"name": config.get("name", "Local File"),
"file_path": config.get("file_path")
}
)
# check filepath given is readable
if not os.access(setup_config["file_path"], os.R_OK):
_LOGGER.error("file path is not readable")
return False
add_devices([
LocalFile(setup_config)
])
class LocalFile(Camera):
"""Local camera."""
def __init__(self, device_info):
"""Initialize Local File Camera component."""
super().__init__()
self._name = device_info["name"]
self._config = device_info
def camera_image(self):
"""Return image response."""
with open(self._config["file_path"], 'rb') as file:
return file.read()
@property
def name(self):
"""Return the name of this camera."""
return self._name

View File

@ -46,10 +46,9 @@ class MjpegCamera(Camera):
return requests.get(self._mjpeg_url,
auth=HTTPBasicAuth(self._username,
self._password),
stream=True)
stream=True, timeout=10)
else:
return requests.get(self._mjpeg_url,
stream=True)
return requests.get(self._mjpeg_url, stream=True, timeout=10)
def camera_image(self):
"""Return a still image response from the camera."""

View File

@ -0,0 +1,104 @@
"""
Support for the Netatmo Welcome camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.netatmo/
"""
import logging
from datetime import timedelta
import requests
from homeassistant.util import Throttle
from homeassistant.components.camera import Camera
from homeassistant.loader import get_component
DEPENDENCIES = ["netatmo"]
_LOGGER = logging.getLogger(__name__)
CONF_HOME = 'home'
ATTR_CAMERAS = 'cameras'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup access to Netatmo Welcome cameras."""
netatmo = get_component('netatmo')
home = config.get(CONF_HOME, None)
data = WelcomeData(netatmo.NETATMO_AUTH, home)
for camera_name in data.get_camera_names():
if ATTR_CAMERAS in config:
if camera_name not in config[ATTR_CAMERAS]:
continue
add_devices_callback([WelcomeCamera(data, camera_name, home)])
class WelcomeCamera(Camera):
"""Representation of the images published from Welcome camera."""
def __init__(self, data, camera_name, home):
"""Setup for access to the BloomSky camera images."""
super(WelcomeCamera, self).__init__()
self._data = data
self._camera_name = camera_name
if home:
self._name = home + ' / ' + camera_name
else:
self._name = camera_name
self._vpnurl, self._localurl = self._data.welcomedata.cameraUrls(
camera=camera_name
)
def camera_image(self):
"""Return a still image response from the camera."""
try:
if self._localurl:
response = requests.get('{0}/live/snapshot_720.jpg'.format(
self._localurl), timeout=10)
else:
response = requests.get('{0}/live/snapshot_720.jpg'.format(
self._vpnurl), timeout=10)
except requests.exceptions.RequestException as error:
_LOGGER.error('Welcome VPN url changed: %s', error)
self._data.update()
(self._vpnurl, self._localurl) = \
self._data.welcomedata.cameraUrls(camera=self._camera_name)
return None
return response.content
@property
def name(self):
"""Return the name of this Netatmo Welcome device."""
return self._name
class WelcomeData(object):
"""Get the latest data from NetAtmo."""
def __init__(self, auth, home=None):
"""Initialize the data object."""
self.auth = auth
self.welcomedata = None
self.camera_names = []
self.home = home
def get_camera_names(self):
"""Return all module available on the API as a list."""
self.update()
if not self.home:
for home in self.welcomedata.cameras.keys():
for camera in self.welcomedata.cameras[home].values():
self.camera_names.append(camera['name'])
else:
for camera in self.welcomedata.cameras[self.home].values():
self.camera_names.append(camera['name'])
return self.camera_names
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Call the NetAtmo API to update the data."""
import lnetatmo
self.welcomedata = lnetatmo.WelcomeData(self.auth)

View File

@ -8,7 +8,7 @@ the user has submitted configuration information.
"""
import logging
from homeassistant.const import EVENT_TIME_CHANGED
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
from homeassistant.helpers.entity import generate_entity_id
DOMAIN = "configurator"
@ -118,6 +118,7 @@ class Configurator(object):
data = {
ATTR_CONFIGURE_ID: request_id,
ATTR_FIELDS: fields,
ATTR_FRIENDLY_NAME: name,
}
data.update({

View File

@ -67,7 +67,9 @@ def setup(hass, config):
lights[1], switches[0], 'input_select.living_room_preset',
'rollershutter.living_room_window', media_players[1],
'scene.romantic_lights'])
group.Group(hass, 'bedroom', [lights[0], switches[1], media_players[0]])
group.Group(hass, 'bedroom', [
lights[0], switches[1], media_players[0],
'input_slider.noise_allowance'])
group.Group(hass, 'kitchen', [
lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])
group.Group(hass, 'doors', [
@ -145,6 +147,17 @@ def setup(hass, config):
{'input_boolean': {'notify': {'icon': 'mdi:car',
'initial': False,
'name': 'Notify Anne Therese is home'}}})
# Set up input boolean
bootstrap.setup_component(
hass, 'input_slider',
{'input_slider': {
'noise_allowance': {'icon': 'mdi:bell-ring',
'min': 0,
'max': 10,
'name': 'Allowed Noise',
'unit_of_measurement': 'dB'}}})
# Set up weblink
bootstrap.setup_component(
hass, 'weblink',

View File

@ -12,10 +12,11 @@ import os
import threading
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.components import discovery, group, zone
from homeassistant.components import group, zone
from homeassistant.components.discovery import SERVICE_NETGEAR
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
import homeassistant.util as util
@ -62,7 +63,7 @@ ATTR_GPS = 'gps'
ATTR_BATTERY = 'battery'
DISCOVERY_PLATFORMS = {
discovery.SERVICE_NETGEAR: 'netgear',
SERVICE_NETGEAR: 'netgear',
}
_LOGGER = logging.getLogger(__name__)
@ -95,8 +96,11 @@ def setup(hass, config):
yaml_path = hass.config.path(YAML_DEVICES)
conf = config.get(DOMAIN, {})
if isinstance(conf, list) and len(conf) > 0:
conf = conf[0]
# Config can be an empty list. In that case, substitute a dict
if isinstance(conf, list):
conf = conf[0] if len(conf) > 0 else {}
consider_home = timedelta(
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
DEFAULT_CONSIDER_HOME))

View File

@ -6,8 +6,10 @@ https://home-assistant.io/components/device_tracker.asuswrt/
"""
import logging
import re
import socket
import telnetlib
import threading
from collections import namedtuple
from datetime import timedelta
from homeassistant.components.device_tracker import DOMAIN
@ -28,6 +30,21 @@ _LEASES_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
r'(?P<host>([^\s]+))')
# command to get both 5GHz and 2.4GHz clients
_WL_CMD = '{ wl -i eth2 assoclist & wl -i eth1 assoclist ; }'
_WL_REGEX = re.compile(
r'\w+\s' +
r'(?P<mac>(([0-9A-F]{2}[:-]){5}([0-9A-F]{2})))')
_ARP_CMD = 'arp -n'
_ARP_REGEX = re.compile(
r'.+\s' +
r'\((?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' +
r'.+\s' +
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' +
r'\s' +
r'.*')
_IP_NEIGH_CMD = 'ip neigh'
_IP_NEIGH_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
@ -41,24 +58,35 @@ _IP_NEIGH_REGEX = re.compile(
def get_scanner(hass, config):
"""Validate the configuration and return an ASUS-WRT scanner."""
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
{DOMAIN: [CONF_HOST, CONF_USERNAME]},
_LOGGER):
return None
elif CONF_PASSWORD not in config[DOMAIN] and \
'pub_key' not in config[DOMAIN]:
_LOGGER.error("Either a public key or password must be provided")
return None
scanner = AsusWrtDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp')
class AsusWrtDeviceScanner(object):
"""This class queries a router running ASUSWRT firmware."""
# pylint: disable=too-many-instance-attributes, too-many-branches
# Eighth attribute needed for mode (AP mode vs router mode)
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = str(config[CONF_USERNAME])
self.password = str(config[CONF_PASSWORD])
self.password = str(config.get(CONF_PASSWORD))
self.pub_key = str(config.get('pub_key'))
self.protocol = config.get('protocol')
self.mode = config.get('mode')
self.lock = threading.Lock()
@ -106,21 +134,40 @@ class AsusWrtDeviceScanner(object):
def ssh_connection(self):
"""Retrieve data from ASUSWRT via the ssh protocol."""
from pexpect import pxssh
from pexpect import pxssh, exceptions
try:
ssh = pxssh.pxssh()
ssh.login(self.host, self.username, self.password)
if self.pub_key:
ssh.login(self.host, self.username, ssh_key=self.pub_key)
elif self.password:
ssh.login(self.host, self.username, self.password)
else:
_LOGGER.error('No password or public key specified')
return None
ssh.sendline(_IP_NEIGH_CMD)
ssh.prompt()
neighbors = ssh.before.split(b'\n')[1:-1]
ssh.sendline(_LEASES_CMD)
ssh.prompt()
leases_result = ssh.before.split(b'\n')[1:-1]
if self.mode == 'ap':
ssh.sendline(_ARP_CMD)
ssh.prompt()
arp_result = ssh.before.split(b'\n')[1:-1]
ssh.sendline(_WL_CMD)
ssh.prompt()
leases_result = ssh.before.split(b'\n')[1:-1]
else:
arp_result = ['']
ssh.sendline(_LEASES_CMD)
ssh.prompt()
leases_result = ssh.before.split(b'\n')[1:-1]
ssh.logout()
return (neighbors, leases_result)
return AsusWrtResult(neighbors, leases_result, arp_result)
except pxssh.ExceptionPxssh as exc:
_LOGGER.exception('Unexpected response from router: %s', exc)
return ('', '')
_LOGGER.error('Unexpected response from router: %s', exc)
return None
except exceptions.EOF:
_LOGGER.error('Connection refused or no route to host')
return None
def telnet_connection(self):
"""Retrieve data from ASUSWRT via the telnet protocol."""
@ -133,47 +180,99 @@ class AsusWrtDeviceScanner(object):
prompt_string = telnet.read_until(b'#').split(b'\n')[-1]
telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1]
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1]
if self.mode == 'ap':
telnet.write('{}\n'.format(_ARP_CMD).encode('ascii'))
arp_result = (telnet.read_until(prompt_string).
split(b'\n')[1:-1])
telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
leases_result = (telnet.read_until(prompt_string).
split(b'\n')[1:-1])
else:
arp_result = ['']
telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
leases_result = (telnet.read_until(prompt_string).
split(b'\n')[1:-1])
telnet.write('exit\n'.encode('ascii'))
return (neighbors, leases_result)
return AsusWrtResult(neighbors, leases_result, arp_result)
except EOFError:
_LOGGER.exception("Unexpected response from router")
return ('', '')
_LOGGER.error("Unexpected response from router")
return None
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router,"
" is telnet enabled?")
return ('', '')
_LOGGER.error("Connection refused by router, is telnet enabled?")
return None
except socket.gaierror as exc:
_LOGGER.error("Socket exception: %s", exc)
return None
except OSError as exc:
_LOGGER.error("OSError: %s", exc)
return None
def get_asuswrt_data(self):
"""Retrieve data from ASUSWRT and return parsed result."""
if self.protocol == 'telnet':
neighbors, leases_result = self.telnet_connection()
if self.protocol == 'ssh':
result = self.ssh_connection()
elif self.protocol == 'telnet':
result = self.telnet_connection()
else:
neighbors, leases_result = self.ssh_connection()
# autodetect protocol
result = self.ssh_connection()
if result:
self.protocol = 'ssh'
else:
result = self.telnet_connection()
if result:
self.protocol = 'telnet'
if not result:
return {}
devices = {}
for lease in leases_result:
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if self.mode == 'ap':
for lease in result.leases:
match = _WL_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse lease row: %s", lease)
continue
if not match:
_LOGGER.warning("Could not parse wl row: %s", lease)
continue
# For leases where the client doesn't set a hostname, ensure it is
# blank and not '*', which breaks the entity_id down the line.
host = match.group('host')
if host == '*':
host = ''
devices[match.group('ip')] = {
'host': host,
'status': '',
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
}
# match mac addresses to IP addresses in ARP table
for arp in result.arp:
if match.group('mac').lower() in arp.decode('utf-8'):
arp_match = _ARP_REGEX.search(arp.decode('utf-8'))
if not arp_match:
_LOGGER.warning("Could not parse arp row: %s", arp)
continue
for neighbor in neighbors:
devices[arp_match.group('ip')] = {
'host': host,
'status': '',
'ip': arp_match.group('ip'),
'mac': match.group('mac').upper(),
}
else:
for lease in result.leases:
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse lease row: %s", lease)
continue
# For leases where the client doesn't set a hostname, ensure it
# is blank and not '*', which breaks entity_id down the line.
host = match.group('host')
if host == '*':
host = ''
devices[match.group('ip')] = {
'host': host,
'status': '',
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
}
for neighbor in result.neighbors:
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse neighbor row: %s", neighbor)

View File

@ -0,0 +1,141 @@
"""
Support for BT Home Hub 5.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.bt_home_hub_5/
"""
import logging
import re
import threading
from datetime import timedelta
import xml.etree.ElementTree as ET
import json
from urllib.parse import unquote
import requests
from homeassistant.helpers import validate_config
from homeassistant.components.device_tracker import DOMAIN
from homeassistant.const import CONF_HOST
from homeassistant.util import Throttle
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
_LOGGER = logging.getLogger(__name__)
_MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})')
# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Return a BT Home Hub 5 scanner if successful."""
if not validate_config(config,
{DOMAIN: [CONF_HOST]},
_LOGGER):
return None
scanner = BTHomeHub5DeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class BTHomeHub5DeviceScanner(object):
"""This class queries a BT Home Hub 5."""
def __init__(self, config):
"""Initialise the scanner."""
_LOGGER.info("Initialising BT Home Hub 5")
self.host = config.get(CONF_HOST, '192.168.1.254')
self.lock = threading.Lock()
self.last_results = {}
self.url = 'http://{}/nonAuth/home_status.xml'.format(self.host)
# Test the router is accessible
data = _get_homehub_data(self.url)
self.success_init = data is not None
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
return (device for device in self.last_results)
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
with self.lock:
# If not initialised and not already scanned and not found.
if device not in self.last_results:
self._update_info()
if not self.last_results:
return None
return self.last_results.get(device)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""Ensure the information from the BT Home Hub 5 is up to date.
Return boolean if scanning successful.
"""
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Scanning")
data = _get_homehub_data(self.url)
if not data:
_LOGGER.warning('Error scanning devices')
return False
self.last_results = data
return True
def _get_homehub_data(url):
"""Retrieve data from BT Home Hub 5 and return parsed result."""
try:
response = requests.get(url, timeout=5)
except requests.exceptions.Timeout:
_LOGGER.exception("Connection to the router timed out")
return
if response.status_code == 200:
return _parse_homehub_response(response.text)
else:
_LOGGER.error("Invalid response from Home Hub: %s", response)
def _parse_homehub_response(data_str):
"""Parse the BT Home Hub 5 data format."""
root = ET.fromstring(data_str)
dirty_json = root.find('known_device_list').get('value')
# Normalise the JavaScript data to JSON.
clean_json = unquote(dirty_json.replace('\'', '\"')
.replace('{', '{\"')
.replace(':\"', '\":\"')
.replace('\",', '\",\"'))
known_devices = [x for x in json.loads(clean_json) if x]
devices = {}
for device in known_devices:
name = device.get('name')
mac = device.get('mac')
if _MAC_REGEX.match(mac) or ',' in mac:
for mac_addr in mac.split(','):
if _MAC_REGEX.match(mac_addr):
devices[mac_addr] = name
else:
devices[mac] = name
return devices

View File

@ -26,7 +26,7 @@ class LocativeView(HomeAssistantView):
"""View to handle locative requests."""
url = "/api/locative"
name = "api:bootstrap"
name = "api:locative"
def __init__(self, hass, see):
"""Initialize Locative url endpoints."""

View File

@ -9,100 +9,30 @@ loaded before the EVENT_PLATFORM_DISCOVERED is fired.
import logging
import threading
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, EVENT_HOMEASSISTANT_START,
EVENT_PLATFORM_DISCOVERED)
from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover
DOMAIN = "discovery"
REQUIREMENTS = ['netdisco==0.6.7']
SCAN_INTERVAL = 300 # seconds
LOAD_PLATFORM = 'load_platform'
SERVICE_WEMO = 'belkin_wemo'
SERVICE_HUE = 'philips_hue'
SERVICE_CAST = 'google_cast'
SERVICE_NETGEAR = 'netgear_router'
SERVICE_SONOS = 'sonos'
SERVICE_PLEX = 'plex_mediaserver'
SERVICE_SQUEEZEBOX = 'logitech_mediaserver'
SERVICE_PANASONIC_VIERA = 'panasonic_viera'
SERVICE_ROKU = 'roku'
SERVICE_HANDLERS = {
SERVICE_WEMO: "wemo",
SERVICE_CAST: "media_player",
SERVICE_HUE: "light",
SERVICE_NETGEAR: 'device_tracker',
SERVICE_SONOS: 'media_player',
SERVICE_PLEX: 'media_player',
SERVICE_SQUEEZEBOX: 'media_player',
SERVICE_PANASONIC_VIERA: 'media_player',
SERVICE_ROKU: 'media_player',
SERVICE_NETGEAR: ('device_tracker', None),
SERVICE_WEMO: ('wemo', None),
'philips_hue': ('light', 'hue'),
'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'),
'roku': ('media_player', 'roku'),
'sonos': ('media_player', 'sonos'),
'logitech_mediaserver': ('media_player', 'squeezebox'),
}
def listen(hass, service, callback):
"""Setup listener for discovery of specific service.
Service can be a string or a list/tuple.
"""
if isinstance(service, str):
service = (service,)
else:
service = tuple(service)
def discovery_event_listener(event):
"""Listen for discovery events."""
if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service:
callback(event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED))
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
def discover(hass, service, discovered=None, component=None, hass_config=None):
"""Fire discovery event. Can ensure a component is loaded."""
if component is not None:
bootstrap.setup_component(hass, component, hass_config)
data = {
ATTR_SERVICE: service
}
if discovered is not None:
data[ATTR_DISCOVERED] = discovered
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)
def load_platform(hass, component, platform, info=None, hass_config=None):
"""Helper method for generic platform loading.
This method allows a platform to be loaded dynamically without it being
known at runtime (in the DISCOVERY_PLATFORMS list of the component).
Advantages of using this method:
- Any component & platforms combination can be dynamically added
- A component (i.e. light) does not have to import every component
that can dynamically add a platform (e.g. wemo, wink, insteon_hub)
- Custom user components can take advantage of discovery/loading
Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be
fired to load the platform. The event will contain:
{ ATTR_SERVICE = LOAD_PLATFORM + '.' + <<component>>
ATTR_DISCOVERED = {LOAD_PLATFORM: <<platform>>} }
* dev note: This listener can be found in entity_component.py
"""
if info is None:
info = {LOAD_PLATFORM: platform}
else:
info[LOAD_PLATFORM] = platform
discover(hass, LOAD_PLATFORM + '.' + component, info, component,
hass_config)
def setup(hass, config):
"""Start a discovery service."""
logger = logging.getLogger(__name__)
@ -119,20 +49,18 @@ def setup(hass, config):
with lock:
logger.info("Found new service: %s %s", service, info)
component = SERVICE_HANDLERS.get(service)
comp_plat = SERVICE_HANDLERS.get(service)
# We do not know how to handle this service.
if not component:
if not comp_plat:
return
# This component cannot be setup.
if not bootstrap.setup_component(hass, component, config):
return
component, platform = comp_plat
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: service,
ATTR_DISCOVERED: info
})
if platform is None:
discover(hass, service, info, component, config)
else:
load_platform(hass, component, platform, info, config)
# pylint: disable=unused-argument
def start_discovery(event):

View File

@ -8,21 +8,18 @@ import logging
import os
from datetime import timedelta
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, CONF_API_KEY, EVENT_PLATFORM_DISCOVERED)
from homeassistant.helpers import discovery
from homeassistant.const import CONF_API_KEY
from homeassistant.loader import get_component
from homeassistant.util import Throttle
DOMAIN = "ecobee"
DISCOVER_THERMOSTAT = "ecobee.thermostat"
DISCOVER_SENSORS = "ecobee.sensor"
NETWORK = None
HOLD_TEMP = 'hold_temp'
REQUIREMENTS = [
'https://github.com/nkgilley/python-ecobee-api/archive/'
'4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5']
'4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6']
_LOGGER = logging.getLogger(__name__)
@ -70,23 +67,11 @@ def setup_ecobee(hass, network, config):
configurator = get_component('configurator')
configurator.request_done(_CONFIGURING.pop('ecobee'))
# Ensure component is loaded
bootstrap.setup_component(hass, 'thermostat', config)
bootstrap.setup_component(hass, 'sensor', config)
hold_temp = config[DOMAIN].get(HOLD_TEMP, False)
# Fire thermostat discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: DISCOVER_THERMOSTAT,
ATTR_DISCOVERED: {'hold_temp': hold_temp}
})
# Fire sensor discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: DISCOVER_SENSORS,
ATTR_DISCOVERED: {}
})
discovery.load_platform(hass, 'thermostat', DOMAIN,
{'hold_temp': hold_temp}, config)
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
# pylint: disable=too-few-public-methods

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@ -1 +1 @@
Subproject commit 0e6dc25fcd09ad1150aab258f8d01491a8ee8db7
Subproject commit 168706fdb192219d8074d6709c0ce686180d1c8a

View File

@ -4,6 +4,7 @@
"start_url": "/",
"display": "standalone",
"theme_color": "#03A9F4",
"background_color": "#FFFFFF",
"icons": [
{
"src": "/static/favicon-192x192.png",
@ -14,6 +15,16 @@
"src": "/static/favicon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/static/favicon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/static/favicon-1024x1024.png",
"sizes": "1024x1024",
"type": "image/png"
}
]
}

View File

@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN, SERVICE_CLOSE, SERVICE_OPEN,
ATTR_ENTITY_ID)
from homeassistant.components import (group, wink)
from homeassistant.components import group
DOMAIN = 'garage_door'
SCAN_INTERVAL = 30
@ -27,11 +27,6 @@ ENTITY_ID_ALL_GARAGE_DOORS = group.ENTITY_ID_FORMAT.format('all_garage_doors')
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
wink.DISCOVER_GARAGE_DOORS: 'wink'
}
GARAGE_DOOR_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
@ -60,8 +55,7 @@ def open_door(hass, entity_id=None):
def setup(hass, config):
"""Track states and offer events for garage door."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
GROUP_NAME_ALL_GARAGE_DOORS)
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_GARAGE_DOORS)
component.setup(config)
def handle_garage_door_service(service):

View File

@ -9,7 +9,7 @@ import logging
from homeassistant.components.garage_door import GarageDoorDevice
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
REQUIREMENTS = ['python-wink==0.7.6']
REQUIREMENTS = ['python-wink==0.7.7']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -39,7 +39,7 @@ def _conf_preprocess(value):
return value
_SINGLE_GROUP_CONFIG = vol.Schema(vol.All(_conf_preprocess, {
vol.Optional(CONF_ENTITIES): vol.Any(None, cv.entity_ids),
vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None),
CONF_VIEW: bool,
CONF_NAME: str,
CONF_ICON: cv.icon,

View File

@ -14,7 +14,6 @@ import homeassistant.util as util
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.temperature import convert
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components import zwave
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
TEMP_CELCIUS)
@ -57,10 +56,6 @@ ATTR_SWING_LIST = "swing_list"
_LOGGER = logging.getLogger(__name__)
DISCOVERY_PLATFORMS = {
zwave.DISCOVER_HVAC: 'zwave'
}
def set_away_mode(hass, away_mode, entity_id=None):
"""Turn all or specified hvac away mode on."""
@ -139,8 +134,7 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
# pylint: disable=too-many-branches
def setup(hass, config):
"""Setup hvacs."""
component = EntityComponent(_LOGGER, DOMAIN, hass,
SCAN_INTERVAL, DISCOVERY_PLATFORMS)
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)
descriptions = load_yaml_config_file(

View File

@ -8,7 +8,7 @@ import logging
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.const import ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
@ -68,18 +68,18 @@ def setup(hass, config):
name = cfg.get(CONF_NAME)
minimum = cfg.get(CONF_MIN)
maximum = cfg.get(CONF_MAX)
state = cfg.get(CONF_INITIAL)
step = cfg.get(CONF_STEP)
state = cfg.get(CONF_INITIAL, minimum)
step = cfg.get(CONF_STEP, 1)
icon = cfg.get(CONF_ICON)
unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT)
if state < minimum:
state = minimum
if state > maximum:
state = maximum
entities.append(
InputSlider(object_id, name, state, minimum, maximum, step, icon)
)
entities.append(InputSlider(object_id, name, state, minimum, maximum,
step, icon, unit))
if not entities:
return False
@ -103,8 +103,9 @@ def setup(hass, config):
class InputSlider(Entity):
"""Represent an slider."""
# pylint: disable=too-many-arguments
def __init__(self, object_id, name, state, minimum, maximum, step, icon):
# pylint: disable=too-many-arguments, too-many-instance-attributes
def __init__(self, object_id, name, state, minimum, maximum, step, icon,
unit):
"""Initialize a select input."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name
@ -113,6 +114,7 @@ class InputSlider(Entity):
self._maximum = maximum
self._step = step
self._icon = icon
self._unit = unit
@property
def should_poll(self):
@ -134,6 +136,11 @@ class InputSlider(Entity):
"""State of the component."""
return self._current_value
@property
def unit_of_measurement(self):
"""Unit of measurement of slider."""
return self._unit
@property
def state_attributes(self):
"""State attributes."""

View File

@ -6,17 +6,12 @@ https://home-assistant.io/components/insteon_hub/
"""
import logging
import homeassistant.bootstrap as bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME,
EVENT_PLATFORM_DISCOVERED)
from homeassistant.helpers import validate_config
from homeassistant.loader import get_component
from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import validate_config, discovery
DOMAIN = "insteon_hub"
REQUIREMENTS = ['insteon_hub==0.4.5']
INSTEON = None
DISCOVER_LIGHTS = "insteon_hub.lights"
_LOGGER = logging.getLogger(__name__)
@ -44,11 +39,7 @@ def setup(hass, config):
_LOGGER.error("Could not connect to Insteon service.")
return
comp_name = 'light'
discovery = DISCOVER_LIGHTS
component = get_component(comp_name)
bootstrap.setup_component(hass, component.DOMAIN, config)
hass.bus.fire(
EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: discovery, ATTR_DISCOVERED: {}})
for component in 'light':
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True

View File

@ -7,19 +7,15 @@ https://home-assistant.io/components/isy994/
import logging
from urllib.parse import urlparse
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED)
from homeassistant.helpers import validate_config
CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import validate_config, discovery
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.loader import get_component
DOMAIN = "isy994"
REQUIREMENTS = ['PyISY==1.0.6']
DISCOVER_LIGHTS = "isy994.lights"
DISCOVER_SWITCHES = "isy994.switches"
DISCOVER_SENSORS = "isy994.sensors"
ISY = None
SENSOR_STRING = 'Sensor'
HIDDEN_STRING = '{HIDE ME}'
@ -76,15 +72,9 @@ def setup(hass, config):
# Listen for HA stop to disconnect.
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop)
# Load components for the devices in the ISY controller that we support.
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
('light', DISCOVER_LIGHTS),
('switch', DISCOVER_SWITCHES))):
component = get_component(comp_name)
bootstrap.setup_component(hass, component.DOMAIN, config)
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: discovery,
ATTR_DISCOVERED: {}})
# Load platforms for the devices in the ISY controller that we support.
for component in ('sensor', 'light', 'switch'):
discovery.load_platform(hass, component, DOMAIN, {}, config)
ISY.auto_update = True
return True

View File

@ -10,9 +10,7 @@ import csv
import voluptuous as vol
from homeassistant.components import (
group, discovery, wemo, wink, isy994,
zwave, insteon_hub, mysensors, tellstick, vera)
from homeassistant.components import group
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
@ -60,19 +58,6 @@ EFFECT_WHITE = "white"
LIGHT_PROFILES_FILE = "light_profiles.csv"
# Maps discovered services to their platforms.
DISCOVERY_PLATFORMS = {
wemo.DISCOVER_LIGHTS: 'wemo',
wink.DISCOVER_LIGHTS: 'wink',
insteon_hub.DISCOVER_LIGHTS: 'insteon_hub',
isy994.DISCOVER_LIGHTS: 'isy994',
discovery.SERVICE_HUE: 'hue',
zwave.DISCOVER_LIGHTS: 'zwave',
mysensors.DISCOVER_LIGHTS: 'mysensors',
tellstick.DISCOVER_LIGHTS: 'tellstick',
vera.DISCOVER_LIGHTS: 'vera',
}
PROP_TO_ATTR = {
'brightness': ATTR_BRIGHTNESS,
'color_temp': ATTR_COLOR_TEMP,
@ -172,8 +157,7 @@ def toggle(hass, entity_id=None, transition=None):
def setup(hass, config):
"""Expose light control via statemachine and services."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
GROUP_NAME_ALL_LIGHTS)
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS)
component.setup(config)
# Load built-in profiles and custom profiles

View File

@ -13,7 +13,7 @@ from homeassistant.util import color as color_util
from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin
REQUIREMENTS = ['python-wink==0.7.6']
REQUIREMENTS = ['python-wink==0.7.7']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):

View File

@ -65,6 +65,7 @@ class LircInterface(threading.Thread):
def run(self):
"""Main loop of LIRC interface thread."""
import lirc
_LOGGER.debug('LIRC interface thread started')
while not self.stopped.isSet():
try:
code = lirc.nextcode() # list; empty if no buttons pressed
@ -80,4 +81,5 @@ class LircInterface(threading.Thread):
{BUTTON_NAME: code})
else:
time.sleep(0.2)
_LOGGER.info('LIRC interface thread stopped')
lirc.deinit()
_LOGGER.debug('LIRC interface thread stopped')

View File

@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED,
STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK)
from homeassistant.components import (group, verisure, wink, zwave)
from homeassistant.components import group
DOMAIN = 'lock'
SCAN_INTERVAL = 30
@ -30,13 +30,6 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
wink.DISCOVER_LOCKS: 'wink',
verisure.DISCOVER_LOCKS: 'verisure',
zwave.DISCOVER_LOCKS: 'zwave',
}
LOCK_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(ATTR_CODE): cv.string,
@ -76,8 +69,7 @@ def unlock(hass, entity_id=None, code=None):
def setup(hass, config):
"""Track states and offer events for locks."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
GROUP_NAME_ALL_LOCKS)
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LOCKS)
component.setup(config)
def handle_lock_service(service):

View File

@ -9,7 +9,7 @@ import logging
from homeassistant.components.lock import LockDevice
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
REQUIREMENTS = ['python-wink==0.7.6']
REQUIREMENTS = ['python-wink==0.7.7']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -9,7 +9,6 @@ import os
import voluptuous as vol
from homeassistant.components import discovery
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
@ -30,15 +29,6 @@ SCAN_INTERVAL = 10
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DISCOVERY_PLATFORMS = {
discovery.SERVICE_CAST: 'cast',
discovery.SERVICE_SONOS: 'sonos',
discovery.SERVICE_PLEX: 'plex',
discovery.SERVICE_SQUEEZEBOX: 'squeezebox',
discovery.SERVICE_PANASONIC_VIERA: 'panasonic_viera',
discovery.SERVICE_ROKU: 'roku',
}
SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source'
@ -285,8 +275,7 @@ def select_source(hass, source, entity_id=None):
def setup(hass, config):
"""Track states and offer events for media_players."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)

View File

@ -11,7 +11,7 @@ from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice)
SUPPORT_STOP, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_UNKNOWN)
@ -21,7 +21,7 @@ CONF_IGNORE_CEC = 'ignore_cec'
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA
SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_STOP
KNOWN_HOSTS = []
DEFAULT_PORT = 8009
@ -241,6 +241,10 @@ class CastDevice(MediaPlayerDevice):
"""Send pause command."""
self.cast.media_controller.pause()
def media_stop(self):
"""Send stop command."""
self.cast.media_controller.stop()
def media_previous_track(self):
"""Send previous track command."""
self.cast.media_controller.rewind()

View File

@ -0,0 +1,368 @@
"""
Component for controlling Pandora stations through the pianobar client.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/media_player.pandora/
"""
import logging
import re
import os
import signal
from datetime import timedelta
import shutil
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, MEDIA_TYPE_MUSIC,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_SELECT_SOURCE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PLAY_PAUSE,
SERVICE_MEDIA_PLAY, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN,
MediaPlayerDevice)
from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_IDLE)
from homeassistant import util
REQUIREMENTS = ['pexpect==4.0.1']
_LOGGER = logging.getLogger(__name__)
# SUPPORT_VOLUME_SET is close to available but we need volume up/down
# controls in the GUI.
PANDORA_SUPPORT = \
SUPPORT_PAUSE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_NEXT_TRACK | \
SUPPORT_SELECT_SOURCE
CMD_MAP = {SERVICE_MEDIA_NEXT_TRACK: 'n',
SERVICE_MEDIA_PLAY_PAUSE: 'p',
SERVICE_MEDIA_PLAY: 'p',
SERVICE_VOLUME_UP: ')',
SERVICE_VOLUME_DOWN: '('}
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=2)
CURRENT_SONG_PATTERN = re.compile(r'"(.*?)"\s+by\s+"(.*?)"\son\s+"(.*?)"',
re.MULTILINE)
STATION_PATTERN = re.compile(r'Station\s"(.+?)"', re.MULTILINE)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the media player pandora platform."""
if not _pianobar_exists():
return False
pandora = PandoraMediaPlayer('Pandora')
# make sure we end the pandora subprocess on exit in case user doesn't
# power it down.
def _stop_pianobar(_event):
pandora.turn_off()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_pianobar)
add_devices([pandora])
# pylint: disable=too-many-instance-attributes
class PandoraMediaPlayer(MediaPlayerDevice):
"""A media player that uses the Pianobar interface to Pandora."""
# pylint: disable=abstract-method
def __init__(self, name):
"""Initialize the demo device."""
MediaPlayerDevice.__init__(self)
self._name = name
self._player_state = STATE_OFF
self._station = ''
self._media_title = ''
self._media_artist = ''
self._media_album = ''
self._stations = []
self._time_remaining = 0
self._media_duration = 0
self._pianobar = None
@property
def should_poll(self):
"""Should be polled for current state."""
return True
@property
def name(self):
"""Return the name of the media player."""
return self._name
@property
def state(self):
"""Return the state of the player."""
return self._player_state
def turn_on(self):
"""Turn the media player on."""
import pexpect
if self._player_state != STATE_OFF:
return
self._pianobar = pexpect.spawn('pianobar')
_LOGGER.info('Started pianobar subprocess')
mode = self._pianobar.expect(['Receiving new playlist',
'Select station:',
'Email:'])
if mode == 1:
# station list was presented. dismiss it.
self._pianobar.sendcontrol('m')
elif mode == 2:
_LOGGER.warning('The pianobar client is not configured to log in. '
'Please create a config file for it as described '
'at https://home-assistant.io'
'/components/media_player.pandora/')
# pass through the email/password prompts to quit cleanly
self._pianobar.sendcontrol('m')
self._pianobar.sendcontrol('m')
self._pianobar.terminate()
self._pianobar = None
return
self._update_stations()
self.update_playing_status()
self._player_state = STATE_IDLE
self.update_ha_state()
def turn_off(self):
"""Turn the media player off."""
import pexpect
if self._pianobar is None:
_LOGGER.info('Pianobar subprocess already stopped')
return
self._pianobar.send('q')
try:
_LOGGER.info('Stopped Pianobar subprocess')
self._pianobar.terminate()
except pexpect.exceptions.TIMEOUT:
# kill the process group
os.killpg(os.getpgid(self._pianobar.pid), signal.SIGTERM)
_LOGGER.info('Killed Pianobar subprocess')
self._pianobar = None
self._player_state = STATE_OFF
self.update_ha_state()
def media_play(self):
"""Send play command."""
self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE)
self._player_state = STATE_PLAYING
self.update_ha_state()
def media_pause(self):
"""Send pause command."""
self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE)
self._player_state = STATE_PAUSED
self.update_ha_state()
def media_next_track(self):
"""Go to next track."""
self._send_pianobar_command(SERVICE_MEDIA_NEXT_TRACK)
self.update_ha_state()
@property
def supported_media_commands(self):
"""Show what this supports."""
return PANDORA_SUPPORT
@property
def source(self):
"""Name of the current input source."""
return self._station
@property
def source_list(self):
"""List of available input sources."""
return self._stations
@property
def media_title(self):
"""Title of current playing media."""
self.update_playing_status()
return self._media_title
@property
def media_content_type(self):
"""Content type of current playing media."""
return MEDIA_TYPE_MUSIC
@property
def media_artist(self):
"""Artist of current playing media, music track only."""
return self._media_artist
@property
def media_album_name(self):
"""Album name of current playing media, music track only."""
return self._media_album
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
return self._media_duration
def select_source(self, source):
"""Choose a different Pandora station and play it."""
try:
station_index = self._stations.index(source)
except ValueError:
_LOGGER.warning('Station `%s` is not in list', source)
return
_LOGGER.info('Setting station %s, %d', source, station_index)
self._send_station_list_command()
self._pianobar.sendline('{}'.format(station_index))
self._pianobar.expect('\r\n')
self._player_state = STATE_PLAYING
def _send_station_list_command(self):
"""Send a station list command."""
import pexpect
self._pianobar.send('s')
try:
self._pianobar.expect('Select station:', timeout=1)
except pexpect.exceptions.TIMEOUT:
# try again. Buffer was contaminated.
self._clear_buffer()
self._pianobar.send('s')
self._pianobar.expect('Select station:')
def update_playing_status(self):
"""Query pianobar for info about current media_title, station."""
response = self._query_for_playing_status()
if not response:
return
self._update_current_station(response)
self._update_current_song(response)
self._update_song_position()
def _query_for_playing_status(self):
"""Query system for info about current track."""
import pexpect
self._clear_buffer()
self._pianobar.send('i')
try:
match_idx = self._pianobar.expect([br'(\d\d):(\d\d)/(\d\d):(\d\d)',
'No song playing',
'Select station',
'Receiving new playlist'])
except pexpect.exceptions.EOF:
_LOGGER.info('Pianobar process already exited.')
return None
self._log_match()
if match_idx == 1:
# idle.
response = None
elif match_idx == 2:
# stuck on a station selection dialog. Clear it.
_LOGGER.warning('On unexpected station list page.')
self._pianobar.sendcontrol('m') # press enter
self._pianobar.sendcontrol('m') # do it again b/c an 'i' got in
response = self.update_playing_status()
elif match_idx == 3:
_LOGGER.debug('Received new playlist list.')
response = self.update_playing_status()
else:
response = self._pianobar.before.decode('utf-8')
return response
def _update_current_station(self, response):
"""Update current station."""
station_match = re.search(STATION_PATTERN, response)
if station_match:
self._station = station_match.group(1)
_LOGGER.debug('Got station as: %s', self._station)
else:
_LOGGER.warning('No station match. ')
def _update_current_song(self, response):
"""Update info about current song."""
song_match = re.search(CURRENT_SONG_PATTERN, response)
if song_match:
(self._media_title, self._media_artist,
self._media_album) = song_match.groups()
_LOGGER.debug('Got song as: %s', self._media_title)
else:
_LOGGER.warning('No song match.')
@util.Throttle(MIN_TIME_BETWEEN_UPDATES)
def _update_song_position(self):
"""
Get the song position and duration.
It's hard to predict whether or not the music will start during init
so we have to detect state by checking the ticker.
"""
(cur_minutes, cur_seconds,
total_minutes, total_seconds) = self._pianobar.match.groups()
time_remaining = int(cur_minutes) * 60 + int(cur_seconds)
self._media_duration = int(total_minutes) * 60 + int(total_seconds)
if (time_remaining != self._time_remaining and
time_remaining != self._media_duration):
self._player_state = STATE_PLAYING
elif self._player_state == STATE_PLAYING:
self._player_state = STATE_PAUSED
self._time_remaining = time_remaining
def _log_match(self):
"""Log grabbed values from console."""
_LOGGER.debug('Before: %s\nMatch: %s\nAfter: %s',
repr(self._pianobar.before),
repr(self._pianobar.match),
repr(self._pianobar.after))
def _send_pianobar_command(self, service_cmd):
"""Send a command to Pianobar."""
command = CMD_MAP.get(service_cmd)
_LOGGER.debug('Sending pinaobar command %s for %s',
command, service_cmd)
if command is None:
_LOGGER.info('Command %s not supported yet', service_cmd)
self._clear_buffer()
self._pianobar.sendline(command)
def _update_stations(self):
"""List defined Pandora stations."""
self._send_station_list_command()
station_lines = self._pianobar.before.decode('utf-8')
_LOGGER.debug('Getting stations: %s', station_lines)
self._stations = []
for line in station_lines.split('\r\n'):
match = re.search(r'\d+\).....(.+)', line)
if match:
station = match.group(1).strip()
_LOGGER.debug('Found station %s', station)
self._stations.append(station)
else:
_LOGGER.debug('No station match on `%s`', line)
self._pianobar.sendcontrol('m') # press enter with blank line
self._pianobar.sendcontrol('m') # do it twice in case an 'i' got in
def _clear_buffer(self):
"""
Clear buffer from pexpect.
This is necessary because there are a bunch of 00:00 in the buffer
"""
import pexpect
try:
while not self._pianobar.expect('.+', timeout=0.1):
pass
except pexpect.exceptions.TIMEOUT:
pass
def _pianobar_exists():
"""Verify that Pianobar is properly installed."""
pianobar_exe = shutil.which('pianobar')
if pianobar_exe:
return True
else:
_LOGGER.warning('The Pandora component depends on the Pianobar '
'client, which cannot be found. Please install '
'using instructions at'
'https://home-assistant.io'
'/components/media_player.pandora/')
return False

View File

@ -72,7 +72,7 @@ class SamsungTVDevice(MediaPlayerDevice):
def update(self):
"""Retrieve the latest data."""
# Send an empty key to see if we are still connected
return self.send_key('KEY_POWER')
return self.send_key('KEY')
def get_remote(self):
"""Create or return a remote control instance."""

View File

@ -153,3 +153,19 @@ sonos_group_players:
entity_id:
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
example: 'media_player.living_room_sonos'
sonos_snapshot:
description: Take a snapshot of the media player.
fields:
entity_id:
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
example: 'media_player.living_room_sonos'
sonos_restore:
description: Restore a snapshot of the media player.
fields:
entity_id:
description: Name(s) of entites that will coordinate the grouping. Platform dependent.
example: 'media_player.living_room_sonos'

View File

@ -34,6 +34,8 @@ SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_SEEK
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
SERVICE_SNAPSHOT = 'sonos_snapshot'
SERVICE_RESTORE = 'sonos_restore'
# pylint: disable=unused-argument
@ -84,6 +86,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device.group_players()
device.update_ha_state(True)
def snapshot(service):
"""Take a snapshot."""
entity_id = service.data.get('entity_id')
if entity_id:
_devices = [device for device in devices
if device.entity_id == entity_id]
else:
_devices = devices
for device in _devices:
device.snapshot(service)
device.update_ha_state(True)
def restore(service):
"""Restore a snapshot."""
entity_id = service.data.get('entity_id')
if entity_id:
_devices = [device for device in devices
if device.entity_id == entity_id]
else:
_devices = devices
for device in _devices:
device.restore(service)
device.update_ha_state(True)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
@ -91,6 +121,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
group_players_service,
descriptions.get(SERVICE_GROUP_PLAYERS))
hass.services.register(DOMAIN, SERVICE_SNAPSHOT,
snapshot,
descriptions.get(SERVICE_SNAPSHOT))
hass.services.register(DOMAIN, SERVICE_RESTORE,
restore,
descriptions.get(SERVICE_RESTORE))
return True
@ -136,6 +174,8 @@ class SonosDevice(MediaPlayerDevice):
super(SonosDevice, self).__init__()
self._player = player
self.update()
from soco.snapshot import Snapshot
self.soco_snapshot = Snapshot(self._player)
@property
def should_poll(self):
@ -315,6 +355,16 @@ class SonosDevice(MediaPlayerDevice):
"""Group all players under this coordinator."""
self._player.partymode()
@only_if_coordinator
def snapshot(self, service):
"""Snapshot the player."""
self.soco_snapshot.snapshot()
@only_if_coordinator
def restore(self, service):
"""Restore snapshot for the player."""
self.soco_snapshot.restore(True)
@property
def available(self):
"""Return True if player is reachable, False otherwise."""

View File

@ -7,14 +7,11 @@ https://home-assistant.io/components/sensor.mysensors/
import logging
import socket
import homeassistant.bootstrap as bootstrap
from homeassistant.const import (ATTR_BATTERY_LEVEL, ATTR_DISCOVERED,
ATTR_SERVICE, CONF_OPTIMISTIC,
from homeassistant.const import (ATTR_BATTERY_LEVEL, CONF_OPTIMISTIC,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
EVENT_PLATFORM_DISCOVERED, STATE_OFF,
STATE_ON, TEMP_CELSIUS)
from homeassistant.helpers import validate_config
STATE_OFF, STATE_ON, TEMP_CELSIUS)
from homeassistant.helpers import validate_config, discovery
CONF_GATEWAYS = 'gateways'
CONF_DEVICE = 'device'
@ -40,19 +37,6 @@ ATTR_DEVICE = 'device'
GATEWAYS = None
DISCOVER_SENSORS = 'mysensors.sensors'
DISCOVER_SWITCHES = 'mysensors.switches'
DISCOVER_LIGHTS = 'mysensors.lights'
DISCOVER_BINARY_SENSORS = 'mysensors.binary_sensor'
# Maps discovered services to their platforms
DISCOVERY_COMPONENTS = [
('sensor', DISCOVER_SENSORS),
('switch', DISCOVER_SWITCHES),
('light', DISCOVER_LIGHTS),
('binary_sensor', DISCOVER_BINARY_SENSORS),
]
def setup(hass, config): # pylint: disable=too-many-locals
"""Setup the MySensors component."""
@ -124,14 +108,8 @@ def setup(hass, config): # pylint: disable=too-many-locals
GATEWAYS[device] = setup_gateway(
device, persistence_file, baud_rate, tcp_port)
for (component, discovery_service) in DISCOVERY_COMPONENTS:
# Ensure component is loaded
if not bootstrap.setup_component(hass, component, config):
return False
# Fire discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: discovery_service,
ATTR_DISCOVERED: {}})
for component in 'sensor', 'switch', 'light', 'binary_sensor':
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True

View File

@ -0,0 +1,56 @@
"""
Support for the Netatmo devices (Weather Station and Welcome camera).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/netatmo/
"""
import logging
from urllib.error import HTTPError
from homeassistant.const import (
CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME)
from homeassistant.helpers import validate_config, discovery
REQUIREMENTS = [
'https://github.com/jabesq/netatmo-api-python/archive/'
'v0.5.0.zip#lnetatmo==0.5.0']
_LOGGER = logging.getLogger(__name__)
CONF_SECRET_KEY = 'secret_key'
DOMAIN = "netatmo"
NETATMO_AUTH = None
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
"""Setup the Netatmo devices."""
if not validate_config(config,
{DOMAIN: [CONF_API_KEY,
CONF_USERNAME,
CONF_PASSWORD,
CONF_SECRET_KEY]},
_LOGGER):
return None
import lnetatmo
global NETATMO_AUTH
try:
NETATMO_AUTH = lnetatmo.ClientAuth(config[DOMAIN][CONF_API_KEY],
config[DOMAIN][CONF_SECRET_KEY],
config[DOMAIN][CONF_USERNAME],
config[DOMAIN][CONF_PASSWORD],
"read_station read_camera "
"access_camera")
except HTTPError:
_LOGGER.error(
"Connection error "
"Please check your settings for NatAtmo API.")
return False
for component in 'camera', 'sensor':
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True

View File

@ -14,7 +14,7 @@ from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-telegram-bot==4.2.0']
REQUIREMENTS = ['python-telegram-bot==4.2.1']
def get_service(hass, config):

View File

@ -9,9 +9,8 @@ import logging
import time
import requests
from homeassistant.components import discovery
from homeassistant.const import CONF_API_KEY, CONF_HOST
from homeassistant.helpers import validate_config
from homeassistant.helpers import validate_config, discovery
DOMAIN = "octoprint"
OCTOPRINT = None

View File

@ -29,9 +29,6 @@ ENTITY_ID_ALL_ROLLERSHUTTERS = group.ENTITY_ID_FORMAT.format(
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {}
_LOGGER = logging.getLogger(__name__)
ATTR_CURRENT_POSITION = 'current_position'
@ -68,8 +65,7 @@ def stop(hass, entity_id=None):
def setup(hass, config):
"""Track states and offer events for roller shutters."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
GROUP_NAME_ALL_ROLLERSHUTTERS)
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_ROLLERSHUTTERS)
component.setup(config)
def handle_rollershutter_service(service):

View File

@ -0,0 +1,92 @@
"""
Support for Wink Shades.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/rollershutter.wink/
"""
import logging
from homeassistant.components.rollershutter import RollershutterDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.7']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink rollershutter platform."""
import pywink
if discovery_info is None:
token = config.get(CONF_ACCESS_TOKEN)
if token is None:
logging.getLogger(__name__).error(
"Missing wink access_token. "
"Get one at https://winkbearertoken.appspot.com/")
return
pywink.set_bearer_token(token)
add_devices(WinkRollershutterDevice(shade) for shade in
pywink.get_shades())
class WinkRollershutterDevice(RollershutterDevice):
"""Representation of a Wink rollershutter (shades)."""
def __init__(self, wink):
"""Initialize the rollershutter."""
self.wink = wink
self._battery = None
@property
def should_poll(self):
"""Wink Shades don't track their position."""
return False
@property
def unique_id(self):
"""Return the ID of this wink rollershutter."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the rollershutter if any."""
return self.wink.name()
def update(self):
"""Update the state of the rollershutter."""
return self.wink.update_state()
@property
def available(self):
"""True if connection == True."""
return self.wink.available
def move_down(self):
"""Close the shade."""
self.wink.set_state(0)
def move_up(self):
"""Open the shade."""
self.wink.set_state(1)
@property
def current_position(self):
"""Return current position of roller shutter.
Wink reports blind shade positions as 0 or 1.
home-assistant expects:
None is unknown, 0 is closed, 100 is fully open.
"""
state = self.wink.state()
if state == 0:
return 0
elif state == 1:
return 100
else:
return None
def stop(self):
"""Can't stop Wink rollershutter due to API."""
pass

View File

@ -8,34 +8,17 @@ import logging
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components import (
wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors,
bloomsky, vera)
DOMAIN = 'sensor'
SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}'
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
bloomsky.DISCOVER_SENSORS: 'bloomsky',
wink.DISCOVER_SENSORS: 'wink',
zwave.DISCOVER_SENSORS: 'zwave',
isy994.DISCOVER_SENSORS: 'isy994',
verisure.DISCOVER_SENSORS: 'verisure',
ecobee.DISCOVER_SENSORS: 'ecobee',
tellduslive.DISCOVER_SENSORS: 'tellduslive',
mysensors.DISCOVER_SENSORS: 'mysensors',
vera.DISCOVER_SENSORS: 'vera',
}
def setup(hass, config):
"""Track states and offer events for sensors."""
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)

View File

@ -17,16 +17,18 @@ SENSOR_TYPES = ["Temperature",
"Humidity",
"Pressure",
"Luminance",
"UVIndex"]
"UVIndex",
"Voltage"]
# Sensor units - these do not currently align with the API documentation
SENSOR_UNITS = {"Temperature": TEMP_FAHRENHEIT,
"Humidity": "%",
"Pressure": "inHg",
"Luminance": "cd/m²"}
"Luminance": "cd/m²",
"Voltage": "mV"}
# Which sensors to format numerically
FORMAT_NUMBERS = ["Temperature", "Pressure"]
FORMAT_NUMBERS = ["Temperature", "Pressure", "Voltage"]
# pylint: disable=unused-argument

View File

@ -6,8 +6,12 @@ https://home-assistant.io/components/sensor.forecast/
"""
import logging
from datetime import timedelta
from requests.exceptions import ConnectionError as ConnectError, \
HTTPError, Timeout
from homeassistant.components.sensor import DOMAIN
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS
from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
@ -48,21 +52,12 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Forecast.io sensor."""
import forecastio
# Validate the configuration
if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
return False
try:
forecast = forecastio.load_forecast(config.get(CONF_API_KEY, None),
hass.config.latitude,
hass.config.longitude)
forecast.currently()
except ValueError:
_LOGGER.error(
"Connection error "
"Please check your settings for Forecast.io.")
elif not validate_config({DOMAIN: config},
{DOMAIN: [CONF_API_KEY]}, _LOGGER):
return False
if 'units' in config:
@ -72,43 +67,41 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
else:
units = 'us'
data = ForeCastData(config.get(CONF_API_KEY, None),
hass.config.latitude,
hass.config.longitude,
units)
# Create a data fetcher to support all of the configured sensors. Then make
# the first call to init the data and confirm we can connect.
try:
forecast_data = ForeCastData(
config.get(CONF_API_KEY, None), hass.config.latitude,
hass.config.longitude, units)
forecast_data.update_currently()
except ValueError as error:
_LOGGER.error(error)
return False
dev = []
# Initialize and add all of the sensors.
sensors = []
for variable in config['monitored_conditions']:
if variable not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable)
if variable in SENSOR_TYPES:
sensors.append(ForeCastSensor(forecast_data, variable))
else:
dev.append(ForeCastSensor(data, variable))
_LOGGER.error('Sensor type: "%s" does not exist', variable)
add_devices(dev)
add_devices(sensors)
# pylint: disable=too-few-public-methods
class ForeCastSensor(Entity):
"""Implementation of a Forecast.io sensor."""
def __init__(self, weather_data, sensor_type):
def __init__(self, forecast_data, sensor_type):
"""Initialize the sensor."""
self.client_name = 'Weather'
self._name = SENSOR_TYPES[sensor_type][0]
self.forecast_client = weather_data
self.forecast_data = forecast_data
self.type = sensor_type
self._state = None
self._unit_system = self.forecast_client.unit_system
if self._unit_system == 'si':
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
elif self._unit_system == 'us':
self._unit_of_measurement = SENSOR_TYPES[self.type][2]
elif self._unit_system == 'ca':
self._unit_of_measurement = SENSOR_TYPES[self.type][3]
elif self._unit_system == 'uk':
self._unit_of_measurement = SENSOR_TYPES[self.type][4]
elif self._unit_system == 'uk2':
self._unit_of_measurement = SENSOR_TYPES[self.type][5]
self._unit_of_measurement = None
self.update()
@property
@ -129,75 +122,72 @@ class ForeCastSensor(Entity):
@property
def unit_system(self):
"""Return the unit system of this entity."""
return self._unit_system
return self.forecast_data.unit_system
def update_unit_of_measurement(self):
"""Update units based on unit system."""
unit_index = {
'si': 1,
'us': 2,
'ca': 3,
'uk': 4,
'uk2': 5
}.get(self.unit_system, 1)
self._unit_of_measurement = SENSOR_TYPES[self.type][unit_index]
# pylint: disable=too-many-branches,too-many-statements
def update(self):
"""Get the latest data from Forecast.io and updates the states."""
import forecastio
# Call the API for new forecast data. Each sensor will re-trigger this
# same exact call, but thats fine. We cache results for a short period
# of time to prevent hitting API limits. Note that forecast.io will
# charge users for too many calls in 1 day, so take care when updating.
self.forecast_data.update()
self.update_unit_of_measurement()
self.forecast_client.update()
if self.type == 'minutely_summary':
self.forecast_data.update_minutely()
minutely = self.forecast_data.data_minutely
self._state = getattr(minutely, 'summary', '')
elif self.type == 'hourly_summary':
self.forecast_data.update_hourly()
hourly = self.forecast_data.data_hourly
self._state = getattr(hourly, 'summary', '')
elif self.type == 'daily_summary':
self.forecast_data.update_daily()
daily = self.forecast_data.data_daily
self._state = getattr(daily, 'summary', '')
else:
self.forecast_data.update_currently()
currently = self.forecast_data.data_currently
self._state = self.get_currently_state(currently)
try:
if self.type == 'minutely_summary':
self.forecast_client.update_minutely()
self._state = self.forecast_client.data_minutely.summary
return
def get_currently_state(self, data):
"""
Helper function that returns a new state based on the type.
elif self.type == 'hourly_summary':
self.forecast_client.update_hourly()
self._state = self.forecast_client.data_hourly.summary
return
If the sensor type is unknown, the current state is returned.
"""
lookup_type = convert_to_camel(self.type)
state = getattr(data, lookup_type, 0)
elif self.type == 'daily_summary':
self.forecast_client.update_daily()
self._state = self.forecast_client.data_daily.summary
return
# Some state data needs to be rounded to whole values or converted to
# percentages
if self.type in ['precip_probability', 'cloud_cover', 'humidity']:
return round(state * 100, 1)
elif (self.type in ['dew_point', 'temperature', 'apparent_temperature',
'pressure', 'ozone']):
return round(state, 1)
return state
except forecastio.utils.PropertyUnavailable:
return
self.forecast_client.update_currently()
data = self.forecast_client.data_currently
def convert_to_camel(data):
"""
Convert snake case (foo_bar_bat) to camel case (fooBarBat).
try:
if self.type == 'summary':
self._state = data.summary
elif self.type == 'icon':
self._state = data.icon
elif self.type == 'nearest_storm_distance':
self._state = data.nearestStormDistance
elif self.type == 'nearest_storm_bearing':
self._state = data.nearestStormBearing
elif self.type == 'precip_intensity':
self._state = data.precipIntensity
elif self.type == 'precip_type':
self._state = data.precipType
elif self.type == 'precip_probability':
self._state = round(data.precipProbability * 100, 1)
elif self.type == 'dew_point':
self._state = round(data.dewPoint, 1)
elif self.type == 'temperature':
self._state = round(data.temperature, 1)
elif self.type == 'apparent_temperature':
self._state = round(data.apparentTemperature, 1)
elif self.type == 'wind_speed':
self._state = data.windSpeed
elif self.type == 'wind_bearing':
self._state = data.windBearing
elif self.type == 'cloud_cover':
self._state = round(data.cloudCover * 100, 1)
elif self.type == 'humidity':
self._state = round(data.humidity * 100, 1)
elif self.type == 'pressure':
self._state = round(data.pressure, 1)
elif self.type == 'visibility':
self._state = data.visibility
elif self.type == 'ozone':
self._state = round(data.ozone, 1)
except forecastio.utils.PropertyUnavailable:
pass
This is not pythonic, but needed for certain situations
"""
components = data.split('_')
return components[0] + "".join(x.title() for x in components[1:])
class ForeCastData(object):
@ -226,10 +216,13 @@ class ForeCastData(object):
"""Get the latest data from Forecast.io."""
import forecastio
self.data = forecastio.load_forecast(self._api_key,
self.latitude,
self.longitude,
units=self.units)
try:
self.data = forecastio.load_forecast(self._api_key,
self.latitude,
self.longitude,
units=self.units)
except (ConnectError, HTTPError, Timeout, ValueError) as error:
raise ValueError("Unable to init Forecast.io. - %s", error)
self.unit_system = self.data.json['flags']['units']
@Throttle(MIN_TIME_BETWEEN_UPDATES)

View File

@ -37,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pres.S_POWER: [set_req.V_WATT, set_req.V_KWH],
pres.S_DISTANCE: [set_req.V_DISTANCE],
pres.S_LIGHT_LEVEL: [set_req.V_LIGHT_LEVEL],
pres.S_IR: [set_req.V_IR_SEND, set_req.V_IR_RECEIVE],
pres.S_IR: [set_req.V_IR_RECEIVE],
pres.S_WATER: [set_req.V_FLOW, set_req.V_VOLUME],
pres.S_CUSTOM: [set_req.V_VAR1,
set_req.V_VAR2,

View File

@ -6,18 +6,12 @@ https://home-assistant.io/components/sensor.netatmo/
"""
import logging
from datetime import timedelta
from homeassistant.components.sensor import DOMAIN
from homeassistant.const import (
CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS)
from homeassistant.helpers import validate_config
from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.loader import get_component
REQUIREMENTS = [
'https://github.com/HydrelioxGitHub/netatmo-api-python/archive/'
'43ff238a0122b0939a0dc4e8836b6782913fb6e2.zip'
'#lnetatmo==0.4.0']
DEPENDENCIES = ["netatmo"]
_LOGGER = logging.getLogger(__name__)
@ -32,7 +26,6 @@ SENSOR_TYPES = {
'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'],
}
CONF_SECRET_KEY = 'secret_key'
CONF_STATION = 'station'
ATTR_MODULE = 'modules'
@ -43,29 +36,9 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the NetAtmo sensor."""
if not validate_config({DOMAIN: config},
{DOMAIN: [CONF_API_KEY,
CONF_USERNAME,
CONF_PASSWORD,
CONF_SECRET_KEY]},
_LOGGER):
return None
import lnetatmo
authorization = lnetatmo.ClientAuth(config.get(CONF_API_KEY, None),
config.get(CONF_SECRET_KEY, None),
config.get(CONF_USERNAME, None),
config.get(CONF_PASSWORD, None))
if not authorization:
_LOGGER.error(
"Connection error "
"Please check your settings for NatAtmo API.")
return False
data = NetAtmoData(authorization, config.get(CONF_STATION, None))
"""Setup the available Netatmo weather sensors."""
netatmo = get_component('netatmo')
data = NetAtmoData(netatmo.NETATMO_AUTH, config.get(CONF_STATION, None))
dev = []
try:

View File

@ -158,7 +158,7 @@ class NZBGetSensor(Entity):
return
if "DownloadRate" in self.type and value > 0:
# Convert download rate from bytes/s to mb/s
self._state = value / 1024 / 1024
# Convert download rate from Bytes/s to MBytes/s
self._state = round(value / 1024 / 1024, 2)
else:
self._state = value

View File

@ -10,8 +10,7 @@ from datetime import timedelta
import voluptuous as vol
from homeassistant.const import (CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT,
CONF_PLATFORM, CONF_LATITUDE, CONF_LONGITUDE,
CONF_MONITORED_CONDITIONS)
CONF_PLATFORM, CONF_MONITORED_CONDITIONS)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
@ -34,8 +33,7 @@ PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_API_KEY): vol.Coerce(str),
vol.Optional(CONF_MONITORED_CONDITIONS, default=[]):
[vol.In(SENSOR_TYPES.keys())],
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude
vol.Optional('forecast', default=False): cv.boolean
})
# Return cached results if last scan was less then this time ago.
@ -52,7 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
unit = hass.config.temperature_unit
forecast = config.get('forecast', 0)
forecast = config.get('forecast')
owm = OWM(config.get(CONF_API_KEY, None))
if not owm:
@ -73,7 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
except KeyError:
pass
if forecast == 1:
if forecast:
SENSOR_TYPES['forecast'] = ['Forecast', None]
dev.append(OpenWeatherMapSensor(data, 'forecast', unit))

View File

@ -0,0 +1,99 @@
"""
Support for Plex media server monitoring.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.plex/
"""
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.const import (CONF_NAME, CONF_PLATFORM, CONF_USERNAME,
CONF_PASSWORD, CONF_HOST, CONF_PORT)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['plexapi==1.1.0']
CONF_SERVER = 'server'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'plex',
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SERVER): cv.string,
vol.Optional(CONF_NAME, default='Plex'): cv.string,
vol.Optional(CONF_HOST, default='localhost'): cv.string,
vol.Optional(CONF_PORT, default=32400): vol.All(vol.Coerce(int),
vol.Range(min=1,
max=65535))
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Demo sensors."""
name = config.get(CONF_NAME)
plex_user = config.get(CONF_USERNAME)
plex_password = config.get(CONF_PASSWORD)
plex_server = config.get(CONF_SERVER)
plex_host = config.get(CONF_HOST)
plex_port = config.get(CONF_PORT)
plex_url = 'http://' + plex_host + ':' + str(plex_port)
add_devices([PlexSensor(name, plex_url, plex_user,
plex_password, plex_server)])
class PlexSensor(Entity):
"""Plex now playing sensor."""
# pylint: disable=too-many-arguments
def __init__(self, name, plex_url, plex_user, plex_password, plex_server):
"""Initialize the sensor."""
self._name = name
self._state = 0
self._now_playing = []
if plex_user and plex_password:
from plexapi.myplex import MyPlexUser
user = MyPlexUser.signin(plex_user, plex_password)
server = plex_server if plex_server else user.resources()[0].name
self._server = user.getResource(server).connect()
else:
from plexapi.server import PlexServer
self._server = PlexServer(plex_url)
self.update()
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit this state is expressed in."""
return "Watching"
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {content[0]: content[1] for content in self._now_playing}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update method for plex sensor."""
sessions = self._server.sessions()
now_playing = [(s.user.title, "{0} ({1})".format(s.title, s.year))
for s in sessions]
self._state = len(sessions)
self._now_playing = now_playing

View File

@ -0,0 +1,134 @@
"""
Support for displaying collected data over SNMP.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.snmp/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.helpers.entity import Entity
from homeassistant.const import (CONF_HOST, CONF_PLATFORM, CONF_NAME,
CONF_PORT, ATTR_UNIT_OF_MEASUREMENT)
from homeassistant.util import Throttle
REQUIREMENTS = ['pysnmp==4.3.2']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "SNMP"
DEFAULT_COMMUNITY = "public"
DEFAULT_PORT = "161"
CONF_COMMUNITY = "community"
CONF_BASEOID = "baseoid"
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'snmp',
vol.Optional(CONF_NAME): vol.Coerce(str),
vol.Required(CONF_HOST): vol.Coerce(str),
vol.Optional(CONF_PORT): vol.Coerce(int),
vol.Optional(CONF_COMMUNITY): vol.Coerce(str),
vol.Required(CONF_BASEOID): vol.Coerce(str),
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): vol.Coerce(str),
})
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
# pylint: disable=too-many-locals
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the SNMP sensor."""
from pysnmp.hlapi import (getCmd, CommunityData, SnmpEngine,
UdpTransportTarget, ContextData, ObjectType,
ObjectIdentity)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT, DEFAULT_PORT)
community = config.get(CONF_COMMUNITY, DEFAULT_COMMUNITY)
baseoid = config.get(CONF_BASEOID)
errindication, _, _, _ = next(
getCmd(SnmpEngine(),
CommunityData(community, mpModel=0),
UdpTransportTarget((host, port)),
ContextData(),
ObjectType(ObjectIdentity(baseoid))))
if errindication:
_LOGGER.error('Please check the details in the configuration file')
return False
else:
data = SnmpData(host, port, community, baseoid)
add_devices([SnmpSensor(data,
config.get('name', DEFAULT_NAME),
config.get('unit_of_measurement'))])
class SnmpSensor(Entity):
"""Representation of a SNMP sensor."""
def __init__(self, data, name, unit_of_measurement):
"""Initialize the sensor."""
self.data = data
self._name = name
self._state = None
self._unit_of_measurement = unit_of_measurement
self.update()
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._unit_of_measurement
def update(self):
"""Get the latest data and updates the states."""
self.data.update()
self._state = self.data.value
class SnmpData(object):
"""Get the latest data and update the states."""
# pylint: disable=too-few-public-methods
def __init__(self, host, port, community, baseoid):
"""Initialize the data object."""
self._host = host
self._port = port
self._community = community
self._baseoid = baseoid
self.value = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from the remote SNMP capable host."""
from pysnmp.hlapi import (getCmd, CommunityData, SnmpEngine,
UdpTransportTarget, ContextData, ObjectType,
ObjectIdentity)
errindication, errstatus, errindex, restable = next(
getCmd(SnmpEngine(),
CommunityData(self._community, mpModel=0),
UdpTransportTarget((self._host, self._port)),
ContextData(),
ObjectType(ObjectIdentity(self._baseoid)))
)
if errindication:
_LOGGER.error("SNMP error: %s", errindication)
elif errstatus:
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
errindex and restable[-1][int(errindex) - 1] or '?')
else:
for resrow in restable:
self.value = resrow[-1]

View File

@ -0,0 +1,163 @@
"""
Support for hydrological data from the Federal Office for the Environment FOEN.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.swiss_hydrological_data/
"""
import logging
import collections
from datetime import timedelta
import voluptuous as vol
import requests
from homeassistant.const import (TEMP_CELSIUS, CONF_PLATFORM, CONF_NAME)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = ['beautifulsoup4==4.4.1', 'lxml==3.6.0']
_LOGGER = logging.getLogger(__name__)
_RESOURCE = 'http://www.hydrodaten.admin.ch/en/'
DEFAULT_NAME = 'Water temperature'
CONF_STATION = 'station'
ICON = 'mdi:cup-water'
ATTR_DISCHARGE = 'Discharge'
ATTR_WATERLEVEL = 'Level'
ATTR_DISCHARGE_MEAN = 'Discharge mean'
ATTR_WATERLEVEL_MEAN = 'Level mean'
ATTR_TEMPERATURE_MEAN = 'Temperature mean'
ATTR_DISCHARGE_MAX = 'Discharge max'
ATTR_WATERLEVEL_MAX = 'Level max'
ATTR_TEMPERATURE_MAX = 'Temperature max'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'swiss_hydrological_data',
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_STATION): cv.string,
})
HydroData = collections.namedtuple(
"HydrologicalData",
['discharge', 'waterlevel', 'temperature', 'discharge_mean',
'waterlevel_mean', 'temperature_mean', 'discharge_max', 'waterlevel_max',
'temperature_max'])
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Swiss hydrological sensor."""
station = config.get(CONF_STATION)
name = config.get(CONF_NAME, DEFAULT_NAME)
try:
response = requests.get('{}/{}.html'.format(_RESOURCE, station),
timeout=5)
if not response.ok:
_LOGGER.error('The given station does not seem to exist: %s',
station)
return False
except requests.exceptions.ConnectionError:
_LOGGER.error('The URL is not accessible')
return False
data = HydrologicalData(station)
add_devices([SwissHydrologicalDataSensor(name, data)])
# pylint: disable=too-few-public-methods
class SwissHydrologicalDataSensor(Entity):
"""Implementation of an Swiss hydrological sensor."""
def __init__(self, name, data):
"""Initialize the sensor."""
self.data = data
self._name = name
self._unit_of_measurement = TEMP_CELSIUS
self.update()
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self.data.measurings is not None:
return {
ATTR_DISCHARGE: self.data.measurings.discharge,
ATTR_WATERLEVEL: self.data.measurings.waterlevel,
ATTR_DISCHARGE_MEAN: self.data.measurings.discharge_mean,
ATTR_WATERLEVEL_MEAN: self.data.measurings.waterlevel_mean,
ATTR_TEMPERATURE_MEAN: self.data.measurings.temperature_mean,
ATTR_DISCHARGE_MAX: self.data.measurings.discharge_max,
ATTR_WATERLEVEL_MAX: self.data.measurings.waterlevel_max,
ATTR_TEMPERATURE_MAX: self.data.measurings.temperature_max,
}
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return ICON
# pylint: disable=too-many-branches
def update(self):
"""Get the latest data and update the states."""
self.data.update()
if self.data.measurings is not None:
self._state = self.data.measurings.temperature
# pylint: disable=too-few-public-methods
class HydrologicalData(object):
"""The Class for handling the data retrieval."""
def __init__(self, station):
"""Initialize the data object."""
self.station = station
self.measurings = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from hydrodaten.admin.ch."""
from bs4 import BeautifulSoup
try:
response = requests.get('{}/{}.html'.format(_RESOURCE,
self.station),
timeout=5)
except requests.exceptions.ConnectionError:
_LOGGER.error('Unable to retrieve data')
response = None
try:
tables = BeautifulSoup(response.content,
'lxml').findChildren('table')
rows = tables[0].findChildren(['th', 'tr'])
details = []
for row in rows:
cells = row.findChildren('td')
for cell in cells:
details.append(cell.string)
self.measurings = HydroData._make(details)
except AttributeError:
self.measurings = None

View File

@ -11,7 +11,7 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED,
ATTR_BATTERY_LEVEL)
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['python-wink==0.7.6']
REQUIREMENTS = ['python-wink==0.7.7']
SENSOR_TYPES = ['temperature', 'humidity']

View File

@ -9,6 +9,8 @@ import subprocess
import voluptuous as vol
from homeassistant.helpers import template
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
DOMAIN = 'shell_command'
@ -30,14 +32,38 @@ def setup(hass, config):
def service_handler(call):
"""Execute a shell command service."""
cmd = conf[call.service]
cmd, shell = _parse_command(hass, cmd, call.data)
if cmd is None:
return
try:
subprocess.call(conf[call.service], shell=True,
subprocess.call(cmd, shell=shell,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
except subprocess.SubprocessError:
_LOGGER.exception('Error running command')
_LOGGER.exception('Error running command: %s', cmd)
for name in conf.keys():
hass.services.register(DOMAIN, name, service_handler,
schema=SHELL_COMMAND_SCHEMA)
return True
def _parse_command(hass, cmd, variables):
"""Parse command and fill in any template arguments if necessary."""
cmds = cmd.split()
prog = cmds[0]
args = ' '.join(cmds[1:])
try:
rendered_args = template.render(hass, args, variables=variables)
except TemplateError as ex:
_LOGGER.exception('Error rendering command template: %s', ex)
return None, None
if rendered_args == args:
# no template used. default behavior
shell = True
else:
# template used. Must break into list and use shell=False for security
cmd = [prog] + rendered_args.split()
shell = False
return cmd, shell

View File

@ -15,7 +15,7 @@ from homeassistant.util import dt as dt_util
from homeassistant.util import location as location_util
from homeassistant.const import CONF_ELEVATION
REQUIREMENTS = ['astral==1.1']
REQUIREMENTS = ['astral==1.2']
DOMAIN = "sun"
ENTITY_ID = "sun.sun"

View File

@ -18,9 +18,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
ATTR_ENTITY_ID)
from homeassistant.components import (
group, wemo, wink, isy994, verisure,
zwave, tellduslive, tellstick, mysensors, vera)
from homeassistant.components import group
DOMAIN = 'switch'
SCAN_INTERVAL = 30
@ -35,19 +33,6 @@ ATTR_CURRENT_POWER_MWH = "current_power_mwh"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {
wemo.DISCOVER_SWITCHES: 'wemo',
wink.DISCOVER_SWITCHES: 'wink',
isy994.DISCOVER_SWITCHES: 'isy994',
verisure.DISCOVER_SWITCHES: 'verisure',
zwave.DISCOVER_SWITCHES: 'zwave',
tellduslive.DISCOVER_SWITCHES: 'tellduslive',
mysensors.DISCOVER_SWITCHES: 'mysensors',
tellstick.DISCOVER_SWITCHES: 'tellstick',
vera.DISCOVER_SWITCHES: 'vera',
}
PROP_TO_ATTR = {
'current_power_mwh': ATTR_CURRENT_POWER_MWH,
'today_power_mw': ATTR_TODAY_MWH,
@ -87,8 +72,7 @@ def toggle(hass, entity_id=None):
def setup(hass, config):
"""Track states and offer events for switches."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS,
GROUP_NAME_ALL_SWITCHES)
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_SWITCHES)
component.setup(config)
def handle_switch_service(service):

View File

@ -5,14 +5,27 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.mysensors/
"""
import logging
import os
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components import mysensors
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.components.switch import DOMAIN, SwitchDevice
from homeassistant.config import load_yaml_config_file
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = []
ATTR_IR_CODE = 'V_IR_SEND'
SERVICE_SEND_IR_CODE = 'mysensors_send_ir_code'
SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_IR_CODE): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the mysensors platform for switches."""
@ -32,6 +45,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pres.S_SMOKE: [set_req.V_ARMED],
pres.S_LIGHT: [set_req.V_LIGHT],
pres.S_LOCK: [set_req.V_LOCK_STATUS],
pres.S_IR: [set_req.V_IR_SEND],
}
device_class_map = {
pres.S_DOOR: MySensorsSwitch,
pres.S_MOTION: MySensorsSwitch,
pres.S_SMOKE: MySensorsSwitch,
pres.S_LIGHT: MySensorsSwitch,
pres.S_LOCK: MySensorsSwitch,
pres.S_IR: MySensorsIRSwitch,
}
if float(gateway.version) >= 1.5:
map_sv_types.update({
@ -43,15 +65,53 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pres.S_MOISTURE: [set_req.V_ARMED],
})
map_sv_types[pres.S_LIGHT].append(set_req.V_STATUS)
device_class_map.update({
pres.S_BINARY: MySensorsSwitch,
pres.S_SPRINKLER: MySensorsSwitch,
pres.S_WATER_LEAK: MySensorsSwitch,
pres.S_SOUND: MySensorsSwitch,
pres.S_VIBRATION: MySensorsSwitch,
pres.S_MOISTURE: MySensorsSwitch,
})
devices = {}
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
map_sv_types, devices, add_devices, MySensorsSwitch))
map_sv_types, devices, add_devices, device_class_map))
def send_ir_code_service(service):
"""Set IR code as device state attribute."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
ir_code = service.data.get(ATTR_IR_CODE)
if entity_ids:
_devices = [device for device in devices.values()
if isinstance(device, MySensorsIRSwitch) and
device.entity_id in entity_ids]
else:
_devices = [device for device in devices.values()
if isinstance(device, MySensorsIRSwitch)]
kwargs = {ATTR_IR_CODE: ir_code}
for device in _devices:
device.turn_on(**kwargs)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_SEND_IR_CODE,
send_ir_code_service,
descriptions.get(SERVICE_SEND_IR_CODE),
schema=SEND_IR_CODE_SERVICE_SCHEMA)
class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice):
"""Representation of the value of a MySensors Switch child node."""
@property
def assumed_state(self):
"""Return True if unable to access real state of entity."""
return self.gateway.optimistic
@property
def is_on(self):
"""Return True if switch is on."""
@ -77,7 +137,60 @@ class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice):
self._values[self.value_type] = STATE_OFF
self.update_ha_state()
class MySensorsIRSwitch(MySensorsSwitch):
"""IR switch child class to MySensorsSwitch."""
def __init__(self, *args):
"""Setup instance attributes."""
MySensorsSwitch.__init__(self, *args)
self._ir_code = None
@property
def assumed_state(self):
"""Return True if unable to access real state of entity."""
return self.gateway.optimistic
def is_on(self):
"""Return True if switch is on."""
set_req = self.gateway.const.SetReq
if set_req.V_LIGHT in self._values:
return self._values[set_req.V_LIGHT] == STATE_ON
return False
def turn_on(self, **kwargs):
"""Turn the IR switch on."""
set_req = self.gateway.const.SetReq
if set_req.V_LIGHT not in self._values:
_LOGGER.error('missing value_type: %s at node: %s, child: %s',
set_req.V_LIGHT.name, self.node_id, self.child_id)
return
if ATTR_IR_CODE in kwargs:
self._ir_code = kwargs[ATTR_IR_CODE]
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, self._ir_code)
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_LIGHT, 1)
if self.gateway.optimistic:
# optimistically assume that switch has changed state
self._values[self.value_type] = self._ir_code
self._values[set_req.V_LIGHT] = STATE_ON
self.update_ha_state()
# turn off switch after switch was turned on
self.turn_off()
def turn_off(self):
"""Turn the IR switch off."""
set_req = self.gateway.const.SetReq
if set_req.V_LIGHT not in self._values:
_LOGGER.error('missing value_type: %s at node: %s, child: %s',
set_req.V_LIGHT.name, self.node_id, self.child_id)
return
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_LIGHT, 0)
if self.gateway.optimistic:
# optimistically assume that switch has changed state
self._values[set_req.V_LIGHT] = STATE_OFF
self.update_ha_state()
def update(self):
"""Update the controller with the latest value from a sensor."""
MySensorsSwitch.update(self)
if self.value_type in self._values:
self._ir_code = self._values[self.value_type]

View File

@ -0,0 +1,226 @@
"""
Netio switch component.
The Netio platform allows you to control your [Netio]
(http://www.netio-products.com/en/overview/) Netio4, Netio4 All and Netio 230B.
These are smart outlets controllable through ethernet and/or WiFi that reports
consumptions (Netio4all).
To use these devices in your installation, add the following to your
configuration.yaml file:
```
switch:
- platform: netio
host: netio-living
outlets:
1: "AppleTV"
2: "Htpc"
3: "Lampe Gauche"
4: "Lampe Droite"
- platform: netio
host: 192.168.1.43
port: 1234
username: user
password: pwd
outlets:
1: "Nothing..."
4: "Lampe du fer"
```
To get pushed updates from the netio devices, one can add this lua code in the
device interface as an action triggered on "Netio" "System variables updated"
with an 'Always' schedule:
``
-- this will send socket and consumption status updates via CGI
-- to given address. Associate with 'System variables update' event
-- to get consumption updates when they show up
local address='ha:8123'
local path = '/api/netio/<host>'
local output = {}
for i = 1, 4 do for _, what in pairs({'state', 'consumption',
'cumulatedConsumption', 'consumptionStart'}) do
local varname = string.format('output%d_%s', i, what)
table.insert(output,
varname..'='..tostring(devices.system[varname]):gsub(" ","|"))
end end
local qs = table.concat(output, '&')
local url = string.format('http://%s%s?%s', address, path, qs)
devices.system.CustomCGI{url=url}
```
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.netio/
"""
import logging
from collections import namedtuple
from datetime import timedelta
from homeassistant import util
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME, \
CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, STATE_ON
from homeassistant.helpers import validate_config
from homeassistant.components.switch import SwitchDevice
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
REQUIREMENTS = ['pynetio==0.1.6']
DEFAULT_USERNAME = 'admin'
DEFAULT_PORT = 1234
URL_API_NETIO_EP = "/api/netio/<host>"
CONF_OUTLETS = "outlets"
REQ_CONF = [CONF_HOST, CONF_OUTLETS]
ATTR_TODAY_MWH = "today_mwh"
ATTR_TOTAL_CONSUMPTION_KWH = "total_energy_kwh"
ATTR_CURRENT_POWER_MWH = "current_power_mwh"
ATTR_CURRENT_POWER_W = "current_power_w"
Device = namedtuple('device', ['netio', 'entities'])
DEVICES = {}
ATTR_START_DATE = 'start_date'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Configure the netio platform."""
from pynetio import Netio
if validate_config({"conf": config}, {"conf": [CONF_OUTLETS,
CONF_HOST]}, _LOGGER):
if len(DEVICES) == 0:
hass.wsgi.register_view(NetioApiView)
dev = Netio(config[CONF_HOST],
config.get(CONF_PORT, DEFAULT_PORT),
config.get(CONF_USERNAME, DEFAULT_USERNAME),
config.get(CONF_PASSWORD, DEFAULT_USERNAME))
DEVICES[config[CONF_HOST]] = Device(dev, [])
# Throttle the update for all NetioSwitches of one Netio
dev.update = util.Throttle(MIN_TIME_BETWEEN_SCANS)(dev.update)
for key in config[CONF_OUTLETS]:
switch = NetioSwitch(DEVICES[config[CONF_HOST]].netio, key,
config[CONF_OUTLETS][key])
DEVICES[config[CONF_HOST]].entities.append(switch)
add_devices_callback(DEVICES[config[CONF_HOST]].entities)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, dispose)
return True
def dispose(event):
"""Close connections to Netio Devices."""
for _, value in DEVICES.items():
value.netio.stop()
class NetioApiView(HomeAssistantView):
"""WSGI handler class."""
url = URL_API_NETIO_EP
name = "api:netio"
def get(self, request, host):
"""Request handler."""
data = request.args
states, consumptions, cumulated_consumptions, start_dates = \
[], [], [], []
for i in range(1, 5):
out = 'output%d' % i
states.append(data.get('%s_state' % out) == STATE_ON)
consumptions.append(float(data.get('%s_consumption' % out, 0)))
cumulated_consumptions.append(
float(data.get('%s_cumulatedConsumption' % out, 0)) / 1000)
start_dates.append(data.get('%s_consumptionStart' % out, ""))
_LOGGER.debug('%s: %s, %s, %s since %s', host, states,
consumptions, cumulated_consumptions, start_dates)
ndev = DEVICES[host].netio
ndev.consumptions = consumptions
ndev.cumulated_consumptions = cumulated_consumptions
ndev.states = states
ndev.start_dates = start_dates
for dev in DEVICES[host].entities:
dev.update_ha_state()
return self.json(True)
class NetioSwitch(SwitchDevice):
"""Provide a netio linked switch."""
def __init__(self, netio, outlet, name):
"""Defined to handle throttle."""
self._name = name
self.outlet = outlet
self.netio = netio
@property
def name(self):
"""Netio device's name."""
return self._name
@property
def available(self):
"""Return True if entity is available."""
return not hasattr(self, 'telnet')
def turn_on(self):
"""Turn switch on."""
self._set(True)
def turn_off(self):
"""Turn switch off."""
self._set(False)
def _set(self, value):
val = list('uuuu')
val[self.outlet - 1] = "1" if value else "0"
self.netio.get('port list %s' % ''.join(val))
self.netio.states[self.outlet - 1] = value
self.update_ha_state()
@property
def is_on(self):
"""Return switch's status."""
return self.netio.states[self.outlet - 1]
def update(self):
"""Called by HA."""
self.netio.update()
@property
def state_attributes(self):
"""Return optional state attributes."""
return {ATTR_CURRENT_POWER_W: self.current_power_w,
ATTR_TOTAL_CONSUMPTION_KWH: self.cumulated_consumption_kwh,
ATTR_START_DATE: self.start_date.split('|')[0]}
@property
def current_power_w(self):
"""Return actual power."""
return self.netio.consumptions[self.outlet - 1]
@property
def cumulated_consumption_kwh(self):
"""Total enerygy consumption since start_date."""
return self.netio.cumulated_consumptions[self.outlet - 1]
@property
def start_date(self):
"""Point in time when the energy accumulation started."""
return self.netio.start_dates[self.outlet - 1]

View File

@ -0,0 +1,37 @@
# Describes the format for available switch services
turn_on:
description: Turn a switch on
fields:
entity_id:
description: Name(s) of entities to turn on
example: 'switch.living_room'
turn_off:
description: Turn a switch off
fields:
entity_id:
description: Name(s) of entities to turn off
example: 'switch.living_room'
toggle:
description: Toggles a switch state
fields:
entity_id:
description: Name(s) of entities to toggle
example: 'switch.living_room'
mysensors_send_ir_code:
description: Set an IR code as a state attribute for a MySensors IR device switch and turn the switch on.
fields:
entity_id:
description: Name(s) of entites that should have the IR code set and be turned on. Platform dependent.
example: 'switch.living_room_1_1'
V_IR_SEND:
description: IR code to send
example: '0xC284'

View File

@ -9,7 +9,7 @@ import logging
from homeassistant.components.wink import WinkToggleDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.6']
REQUIREMENTS = ['python-wink==0.7.7']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -7,11 +7,7 @@ https://home-assistant.io/components/tellduslive/
import logging
from datetime import timedelta
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED)
from homeassistant.helpers import validate_config
from homeassistant.loader import get_component
from homeassistant.helpers import validate_config, discovery
from homeassistant.util import Throttle
DOMAIN = "tellduslive"
@ -20,12 +16,6 @@ REQUIREMENTS = ['tellive-py==0.5.2']
_LOGGER = logging.getLogger(__name__)
DISCOVER_SENSORS = "tellduslive.sensors"
DISCOVER_SWITCHES = "tellduslive.switches"
DISCOVERY_TYPES = {"sensor": DISCOVER_SENSORS,
"switch": DISCOVER_SWITCHES}
CONF_PUBLIC_KEY = "public_key"
CONF_PRIVATE_KEY = "private_key"
CONF_TOKEN = "token"
@ -101,16 +91,8 @@ class TelldusLiveData(object):
_LOGGER.info("discovered %d new %s devices",
len(found_devices), component_name)
component = get_component(component_name)
bootstrap.setup_component(self._hass,
component.DOMAIN,
self._config)
discovery_type = DISCOVERY_TYPES[component_name]
self._hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: discovery_type,
ATTR_DISCOVERED: found_devices})
discovery.load_platform(self._hass, component_name, DOMAIN,
found_devices, self._config)
def request(self, what, **params):
"""Send a request to the Tellstick Live API."""

View File

@ -8,11 +8,8 @@ import logging
import threading
import voluptuous as vol
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE,
EVENT_PLATFORM_DISCOVERED, EVENT_HOMEASSISTANT_STOP)
from homeassistant.loader import get_component
from homeassistant.helpers import discovery
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity
DOMAIN = "tellstick"
@ -24,11 +21,6 @@ _LOGGER = logging.getLogger(__name__)
ATTR_SIGNAL_REPETITIONS = "signal_repetitions"
DEFAULT_SIGNAL_REPETITIONS = 1
DISCOVER_SWITCHES = "tellstick.switches"
DISCOVER_LIGHTS = "tellstick.lights"
DISCOVERY_TYPES = {"switch": DISCOVER_SWITCHES,
"light": DISCOVER_LIGHTS}
ATTR_DISCOVER_DEVICES = "devices"
ATTR_DISCOVER_CONFIG = "config"
@ -57,17 +49,11 @@ def _discover(hass, config, found_devices, component_name):
_LOGGER.info("discovered %d new %s devices",
len(found_devices), component_name)
component = get_component(component_name)
bootstrap.setup_component(hass, component.DOMAIN,
config)
signal_repetitions = config[DOMAIN].get(ATTR_SIGNAL_REPETITIONS)
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: DISCOVERY_TYPES[component_name],
ATTR_DISCOVERED: {ATTR_DISCOVER_DEVICES: found_devices,
ATTR_DISCOVER_CONFIG:
signal_repetitions}})
discovery.load_platform(hass, component_name, DOMAIN, {
ATTR_DISCOVER_DEVICES: found_devices,
ATTR_DISCOVER_CONFIG: signal_repetitions}, config)
def setup(hass, config):

View File

@ -15,7 +15,6 @@ from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.temperature import convert
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components import (ecobee, zwave)
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
@ -29,6 +28,7 @@ SCAN_INTERVAL = 60
SERVICE_SET_AWAY_MODE = "set_away_mode"
SERVICE_SET_TEMPERATURE = "set_temperature"
SERVICE_SET_FAN_MODE = "set_fan_mode"
SERVICE_SET_HVAC_MODE = "set_hvac_mode"
STATE_HEAT = "heat"
STATE_COOL = "cool"
@ -37,6 +37,7 @@ STATE_IDLE = "idle"
ATTR_CURRENT_TEMPERATURE = "current_temperature"
ATTR_AWAY_MODE = "away_mode"
ATTR_FAN = "fan"
ATTR_HVAC_MODE = "hvac_mode"
ATTR_MAX_TEMP = "max_temp"
ATTR_MIN_TEMP = "min_temp"
ATTR_TEMPERATURE_LOW = "target_temp_low"
@ -45,11 +46,6 @@ ATTR_OPERATION = "current_operation"
_LOGGER = logging.getLogger(__name__)
DISCOVERY_PLATFORMS = {
ecobee.DISCOVER_THERMOSTAT: 'ecobee',
zwave.DISCOVER_THERMOSTATS: 'zwave'
}
SET_AWAY_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_AWAY_MODE): cv.boolean,
@ -62,6 +58,10 @@ SET_FAN_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_FAN): cv.boolean,
})
SET_HVAC_MODE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_HVAC_MODE): cv.string,
})
def set_away_mode(hass, away_mode, entity_id=None):
@ -98,11 +98,22 @@ def set_fan_mode(hass, fan_mode, entity_id=None):
hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data)
def set_hvac_mode(hass, hvac_mode, entity_id=None):
"""Set specified thermostat hvac mode."""
data = {
ATTR_HVAC_MODE: hvac_mode
}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SET_HVAC_MODE, data)
# pylint: disable=too-many-branches
def setup(hass, config):
"""Setup thermostats."""
component = EntityComponent(_LOGGER, DOMAIN, hass,
SCAN_INTERVAL, DISCOVERY_PLATFORMS)
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)
descriptions = load_yaml_config_file(
@ -164,6 +175,22 @@ def setup(hass, config):
descriptions.get(SERVICE_SET_FAN_MODE),
schema=SET_FAN_MODE_SCHEMA)
def hvac_mode_set_service(service):
"""Set hvac mode on target thermostats."""
target_thermostats = component.extract_from_service(service)
hvac_mode = service.data[ATTR_HVAC_MODE]
for thermostat in target_thermostats:
thermostat.set_hvac_mode(hvac_mode)
thermostat.update_ha_state(True)
hass.services.register(
DOMAIN, SERVICE_SET_HVAC_MODE, hvac_mode_set_service,
descriptions.get(SERVICE_SET_HVAC_MODE),
schema=SET_HVAC_MODE_SCHEMA)
return True
@ -250,6 +277,10 @@ class ThermostatDevice(Entity):
"""Set new target temperature."""
pass
def set_hvac_mode(self, hvac_mode):
"""Set hvac mode."""
pass
def turn_away_mode_on(self):
"""Turn away mode on."""
pass

View File

@ -5,17 +5,29 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/thermostat.ecobee/
"""
import logging
from os import path
import voluptuous as vol
from homeassistant.components import ecobee
from homeassistant.components.thermostat import (
STATE_COOL, STATE_HEAT, STATE_IDLE, ThermostatDevice)
from homeassistant.const import STATE_OFF, STATE_ON, TEMP_FAHRENHEIT
DOMAIN, STATE_COOL, STATE_HEAT, STATE_IDLE, ThermostatDevice)
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, TEMP_FAHRENHEIT)
from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['ecobee']
_LOGGER = logging.getLogger(__name__)
ECOBEE_CONFIG_FILE = 'ecobee.conf'
_CONFIGURING = {}
ATTR_FAN_MIN_ON_TIME = "fan_min_on_time"
SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time"
SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Ecobee Thermostat Platform."""
@ -26,10 +38,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.info(
"Loading ecobee thermostat component with hold_temp set to %s",
hold_temp)
add_devices(Thermostat(data, index, hold_temp)
for index in range(len(data.ecobee.thermostats)))
devices = [Thermostat(data, index, hold_temp)
for index in range(len(data.ecobee.thermostats))]
add_devices(devices)
def fan_min_on_time_set_service(service):
"""Set the minimum fan on time on the target thermostats."""
entity_id = service.data.get('entity_id')
if entity_id:
target_thermostats = [device for device in devices
if device.entity_id == entity_id]
else:
target_thermostats = devices
fan_min_on_time = service.data[ATTR_FAN_MIN_ON_TIME]
for thermostat in target_thermostats:
thermostat.set_fan_min_on_time(str(fan_min_on_time))
thermostat.update_ha_state(True)
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(
DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service,
descriptions.get(SERVICE_SET_FAN_MIN_ON_TIME),
schema=SET_FAN_MIN_ON_TIME_SCHEMA)
# pylint: disable=too-many-public-methods
class Thermostat(ThermostatDevice):
"""A thermostat class for Ecobee."""
@ -127,6 +166,11 @@ class Thermostat(ThermostatDevice):
"""Return current hvac mode ie. auto, auxHeatOnly, cool, heat, off."""
return self.thermostat['settings']['hvacMode']
@property
def fan_min_on_time(self):
"""Return current fan minimum on time."""
return self.thermostat['settings']['fanMinOnTime']
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
@ -135,7 +179,8 @@ class Thermostat(ThermostatDevice):
"humidity": self.humidity,
"fan": self.fan,
"mode": self.mode,
"hvac_mode": self.hvac_mode
"hvac_mode": self.hvac_mode,
"fan_min_on_time": self.fan_min_on_time
}
@property
@ -177,6 +222,11 @@ class Thermostat(ThermostatDevice):
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
self.data.ecobee.set_hvac_mode(self.thermostat_index, mode)
def set_fan_min_on_time(self, fan_min_on_time):
"""Set the minimum fan on time."""
self.data.ecobee.set_fan_min_on_time(self.thermostat_index,
fan_min_on_time)
# Home and Sleep mode aren't used in UI yet:
# def turn_home_mode_on(self):

View File

@ -34,3 +34,15 @@ set_fan_mode:
fan:
description: New value of fan mode
example: true
ecobee_set_fan_min_on_time:
description: Set the minimum time, in minutes, to run the fan each hour
fields:
entity_id:
descriptions: Name(s) of entities to change
example: 'thermostat.ecobee'
fan_min_on_time:
description: New value of fan minimum on time
example: 5

View File

@ -25,6 +25,12 @@ DEVICE_MAPPINGS = {
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_IGNORE
}
COMMAND_CLASS_THERMOSTAT_FAN_STATE = 69 # 0x45
COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 # 0x43
COMMAND_CLASS_SENSOR_MULTILEVEL = 49 # 0x31
COMMAND_CLASS_THERMOSTAT_OPERATING_STATE = 66 # 0x42
COMMAND_CLASS_THERMOSTAT_MODE = 64 # 0x40
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the ZWave thermostats."""
@ -51,7 +57,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
discovery_info, zwave.NETWORK)
# pylint: disable=too-many-arguments
# pylint: disable=too-many-arguments, too-many-instance-attributes
class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
"""Represents a HeatControl thermostat."""
@ -61,11 +67,12 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
from pydispatch import dispatcher
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._node = value.node
self._target_temperature = None
self._index = value.index
self._current_temperature = None
self._current_operation = STATE_IDLE
self._current_operation_state = STATE_IDLE
self._unit = None
self._current_operation_state = STATE_IDLE
self._target_temperature = None
self._current_fan_state = STATE_IDLE
self.update_properties()
# register listener
dispatcher.connect(
@ -79,31 +86,37 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
def update_properties(self):
"""Callback on data change for the registered node/value pair."""
# set point
for _, value in self._node.get_values(class_id=0x43).items():
if int(value.data) != 0:
self._target_temperature = int(value.data)
# Operation
for _, value in self._node.get_values(class_id=0x40).items():
self._current_operation = value.data_as_string
# Current Temp
for _, value in self._node.get_values_for_command_class(0x31).items():
# current Temp
for _, value in self._node.get_values_for_command_class(
COMMAND_CLASS_SENSOR_MULTILEVEL).items():
self._current_temperature = int(value.data)
self._unit = value.units
# COMMAND_CLASS_THERMOSTAT_OPERATING_STATE
for _, value in self._node.get_values(class_id=0x42).items():
# operation state
for _, value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_OPERATING_STATE).items():
self._current_operation_state = value.data_as_string
# target temperature
temps = []
for _, value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).items():
temps.append(int(value.data))
if value.index == self._index:
self._target_temperature = value.data
self._target_temperature_high = max(temps)
self._target_temperature_low = min(temps)
# fan state
for _, value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_FAN_STATE).items():
self._current_fan_state = value.data_as_string
@property
def should_poll(self):
"""No polling on ZWave."""
return False
@property
def is_fan_on(self):
"""Return if the fan is not idle."""
return self._current_operation_state != 'Idle'
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
@ -114,7 +127,6 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
return TEMP_FAHRENHEIT
else:
return unit
return self.hass.config.temperature_unit
@property
def current_temperature(self):
@ -123,17 +135,24 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice):
@property
def operation(self):
"""Return the operation mode."""
return self._current_operation
"""Return current operation ie. heat, cool, idle."""
return self._current_operation_state
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self._target_temperature
@property
def is_fan_on(self):
"""Return true if the fan is on."""
return not (self._current_fan_state == 'Idle' or
self._current_fan_state == STATE_IDLE)
def set_temperature(self, temperature):
"""Set new target temperature."""
# set point
for _, value in self._node.get_values_for_command_class(0x43).items():
if int(value.data) != 0:
for _, value in self._node.get_values_for_command_class(
COMMAND_CLASS_THERMOSTAT_SETPOINT).items():
if int(value.data) != 0 and value.index == self._index:
value.data = temperature

View File

@ -5,16 +5,13 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/vera/
"""
import logging
from collections import defaultdict
from requests.exceptions import RequestException
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_SERVICE, ATTR_DISCOVERED,
EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED)
from homeassistant.helpers import discovery
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
REQUIREMENTS = ['pyvera==0.2.10']
@ -27,28 +24,18 @@ VERA_CONTROLLER = None
CONF_EXCLUDE = 'exclude'
CONF_LIGHTS = 'lights'
BINARY_SENSOR = 'binary_sensor'
SENSOR = 'sensor'
LIGHT = 'light'
SWITCH = 'switch'
DEVICE_CATEGORIES = {
'Sensor': BINARY_SENSOR,
'Temperature Sensor': SENSOR,
'Light Sensor': SENSOR,
'Humidity Sensor': SENSOR,
'Dimmable Switch': LIGHT,
'Switch': SWITCH,
'Armable Sensor': SWITCH,
'On/Off Switch': SWITCH,
'Sensor': 'binary_sensor',
'Temperature Sensor': 'sensor',
'Light Sensor': 'sensor',
'Humidity Sensor': 'sensor',
'Dimmable Switch': 'light',
'Switch': 'switch',
'Armable Sensor': 'switch',
'On/Off Switch': 'switch',
# 'Window Covering': NOT SUPPORTED YET
}
DISCOVER_BINARY_SENSORS = 'vera.binary_sensors'
DISCOVER_SENSORS = 'vera.sensors'
DISCOVER_LIGHTS = 'vera.lights'
DISCOVER_SWITCHES = 'vera.switchs'
VERA_DEVICES = defaultdict(list)
@ -100,19 +87,13 @@ def setup(hass, base_config):
dev_type = DEVICE_CATEGORIES.get(device.category)
if dev_type is None:
continue
if dev_type == SWITCH and device.device_id in lights_ids:
dev_type = LIGHT
if dev_type == 'switch' and device.device_id in lights_ids:
dev_type = 'light'
VERA_DEVICES[dev_type].append(device)
for comp_name, discovery in (((BINARY_SENSOR, DISCOVER_BINARY_SENSORS),
(SENSOR, DISCOVER_SENSORS),
(LIGHT, DISCOVER_LIGHTS),
(SWITCH, DISCOVER_SWITCHES))):
component = get_component(comp_name)
bootstrap.setup_component(hass, component.DOMAIN, base_config)
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: discovery,
ATTR_DISCOVERED: {}})
for component in 'binary_sensor', 'sensor', 'light', 'switch':
discovery.load_platform(hass, component, DOMAIN, {}, base_config)
return True

View File

@ -9,19 +9,11 @@ import threading
import time
from datetime import timedelta
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, CONF_PASSWORD, CONF_USERNAME,
EVENT_PLATFORM_DISCOVERED)
from homeassistant.helpers import validate_config
from homeassistant.loader import get_component
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import validate_config, discovery
from homeassistant.util import Throttle
DOMAIN = "verisure"
DISCOVER_SENSORS = 'verisure.sensors'
DISCOVER_SWITCHES = 'verisure.switches'
DISCOVER_ALARMS = 'verisure.alarm_control_panel'
DISCOVER_LOCKS = 'verisure.lock'
REQUIREMENTS = ['vsure==0.8.1']
@ -43,15 +35,8 @@ def setup(hass, config):
if not HUB.login():
return False
for comp_name, discovery in ((('sensor', DISCOVER_SENSORS),
('switch', DISCOVER_SWITCHES),
('alarm_control_panel', DISCOVER_ALARMS),
('lock', DISCOVER_LOCKS))):
component = get_component(comp_name)
bootstrap.setup_component(hass, component.DOMAIN, config)
hass.bus.fire(EVENT_PLATFORM_DISCOVERED,
{ATTR_SERVICE: discovery,
ATTR_DISCOVERED: {}})
for component in ('sensor', 'switch', 'alarm_control_panel', 'lock'):
discovery.load_platform(hass, component, DOMAIN, {}, config)
return True
@ -142,7 +127,7 @@ class VerisureHub(object):
except AttributeError:
status[overview.deviceLabel] = overview
except self._verisure.Error as ex:
_LOGGER.error('Caught connection error %s, tries to reconnect', ex)
_LOGGER.info('Caught connection error %s, tries to reconnect', ex)
self.reconnect()
def reconnect(self):

View File

@ -6,29 +6,22 @@ https://home-assistant.io/components/wemo/
"""
import logging
from homeassistant.components import discovery
from homeassistant.components.discovery import SERVICE_WEMO
from homeassistant.helpers import discovery
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
REQUIREMENTS = ['pywemo==0.4.3']
DOMAIN = 'wemo'
DISCOVER_LIGHTS = 'wemo.light'
DISCOVER_BINARY_SENSORS = 'wemo.binary_sensor'
DISCOVER_SWITCHES = 'wemo.switch'
# Mapping from Wemo model_name to service.
# Mapping from Wemo model_name to component.
WEMO_MODEL_DISPATCH = {
'Bridge': DISCOVER_LIGHTS,
'Insight': DISCOVER_SWITCHES,
'Maker': DISCOVER_SWITCHES,
'Sensor': DISCOVER_BINARY_SENSORS,
'Socket': DISCOVER_SWITCHES,
'LightSwitch': DISCOVER_SWITCHES
}
WEMO_SERVICE_DISPATCH = {
DISCOVER_LIGHTS: 'light',
DISCOVER_BINARY_SENSORS: 'binary_sensor',
DISCOVER_SWITCHES: 'switch',
'Bridge': 'light',
'Insight': 'switch',
'Maker': 'switch',
'Sensor': 'binary_sensor',
'Socket': 'switch',
'LightSwitch': 'switch'
}
SUBSCRIPTION_REGISTRY = None
@ -64,13 +57,12 @@ def setup(hass, config):
_LOGGER.debug('Discovered unique device %s', serial)
KNOWN_DEVICES.append(serial)
service = WEMO_MODEL_DISPATCH.get(model_name) or DISCOVER_SWITCHES
component = WEMO_SERVICE_DISPATCH.get(service)
component = WEMO_MODEL_DISPATCH.get(model_name, 'switch')
discovery.discover(hass, service, discovery_info,
component, config)
discovery.load_platform(hass, component, DOMAIN, discovery_info,
config)
discovery.listen(hass, discovery.SERVICE_WEMO, discovery_dispatch)
discovery.listen(hass, SERVICE_WEMO, discovery_dispatch)
_LOGGER.info("Scanning for WeMo devices.")
devices = [(device.host, device) for device in pywemo.discover_devices()]
@ -92,5 +84,5 @@ def setup(hass, config):
discovery_info = (device.name, device.model_name, url, device.mac,
device.serialnumber)
discovery.discover(hass, discovery.SERVICE_WEMO, discovery_info)
discovery.discover(hass, SERVICE_WEMO, discovery_info)
return True

View File

@ -6,23 +6,12 @@ https://home-assistant.io/components/wink/
"""
import logging
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, CONF_ACCESS_TOKEN,
EVENT_PLATFORM_DISCOVERED, ATTR_BATTERY_LEVEL)
from homeassistant.helpers import validate_config
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
from homeassistant.helpers import validate_config, discovery
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.loader import get_component
DOMAIN = "wink"
REQUIREMENTS = ['python-wink==0.7.6']
DISCOVER_LIGHTS = "wink.lights"
DISCOVER_SWITCHES = "wink.switches"
DISCOVER_SENSORS = "wink.sensors"
DISCOVER_BINARY_SENSORS = "wink.binary_sensors"
DISCOVER_LOCKS = "wink.locks"
DISCOVER_GARAGE_DOORS = "wink.garage_doors"
REQUIREMENTS = ['python-wink==0.7.7']
def setup(hass, config):
@ -36,28 +25,18 @@ def setup(hass, config):
pywink.set_bearer_token(config[DOMAIN][CONF_ACCESS_TOKEN])
# Load components for the devices in the Wink that we support
for component_name, func_exists, discovery_type in (
('light', pywink.get_bulbs, DISCOVER_LIGHTS),
('switch', lambda: pywink.get_switches or
pywink.get_sirens or
pywink.get_powerstrip_outlets, DISCOVER_SWITCHES),
('binary_sensor', pywink.get_sensors, DISCOVER_BINARY_SENSORS),
('sensor', lambda: pywink.get_sensors or
pywink.get_eggtrays, DISCOVER_SENSORS),
('lock', pywink.get_locks, DISCOVER_LOCKS),
('garage_door', pywink.get_garage_doors, DISCOVER_GARAGE_DOORS)):
for component_name, func_exists in (
('light', pywink.get_bulbs),
('switch', lambda: pywink.get_switches or pywink.get_sirens or
pywink.get_powerstrip_outlets),
('binary_sensor', pywink.get_sensors),
('sensor', lambda: pywink.get_sensors or pywink.get_eggtrays),
('lock', pywink.get_locks),
('rollershutter', pywink.get_shades),
('garage_door', pywink.get_garage_doors)):
if func_exists():
component = get_component(component_name)
# Ensure component is loaded
bootstrap.setup_component(hass, component.DOMAIN, config)
# Fire discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: discovery_type,
ATTR_DISCOVERED: {}
})
discovery.load_platform(hass, component_name, DOMAIN, {}, config)
return True

View File

@ -9,11 +9,11 @@ import os.path
import time
from pprint import pprint
from homeassistant import bootstrap
from homeassistant.helpers import discovery
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_DISCOVERED, ATTR_ENTITY_ID, ATTR_LOCATION,
ATTR_SERVICE, CONF_CUSTOMIZE, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_PLATFORM_DISCOVERED)
ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_LOCATION,
CONF_CUSTOMIZE, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers.event import track_time_change
from homeassistant.util import convert, slugify
@ -37,14 +37,6 @@ SERVICE_HEAL_NETWORK = "heal_network"
SERVICE_SOFT_RESET = "soft_reset"
SERVICE_TEST_NETWORK = "test_network"
DISCOVER_SENSORS = "zwave.sensors"
DISCOVER_SWITCHES = "zwave.switch"
DISCOVER_LIGHTS = "zwave.light"
DISCOVER_BINARY_SENSORS = 'zwave.binary_sensor'
DISCOVER_THERMOSTATS = 'zwave.thermostat'
DISCOVER_HVAC = 'zwave.hvac'
DISCOVER_LOCKS = 'zwave.lock'
EVENT_SCENE_ACTIVATED = "zwave.scene_activated"
COMMAND_CLASS_SWITCH_MULTILEVEL = 38
@ -71,39 +63,32 @@ TYPE_DECIMAL = "Decimal"
# value type).
DISCOVERY_COMPONENTS = [
('sensor',
DISCOVER_SENSORS,
[COMMAND_CLASS_SENSOR_MULTILEVEL,
COMMAND_CLASS_METER,
COMMAND_CLASS_ALARM],
TYPE_WHATEVER,
GENRE_USER),
('light',
DISCOVER_LIGHTS,
[COMMAND_CLASS_SWITCH_MULTILEVEL],
TYPE_BYTE,
GENRE_USER),
('switch',
DISCOVER_SWITCHES,
[COMMAND_CLASS_SWITCH_BINARY],
TYPE_BOOL,
GENRE_USER),
('binary_sensor',
DISCOVER_BINARY_SENSORS,
[COMMAND_CLASS_SENSOR_BINARY],
TYPE_BOOL,
GENRE_USER),
('thermostat',
DISCOVER_THERMOSTATS,
[COMMAND_CLASS_THERMOSTAT_SETPOINT],
TYPE_WHATEVER,
GENRE_WHATEVER),
('hvac',
DISCOVER_HVAC,
[COMMAND_CLASS_THERMOSTAT_FAN_MODE],
TYPE_WHATEVER,
GENRE_WHATEVER),
('lock',
DISCOVER_LOCKS,
[COMMAND_CLASS_DOOR_LOCK],
TYPE_BOOL,
GENRE_USER),
@ -235,7 +220,6 @@ def setup(hass, config):
def value_added(node, value):
"""Called when a value is added to a node on the network."""
for (component,
discovery_service,
command_ids,
value_type,
value_genre) in DISCOVERY_COMPONENTS:
@ -247,9 +231,6 @@ def setup(hass, config):
if value_genre is not None and value_genre != value.genre:
continue
# Ensure component is loaded
bootstrap.setup_component(hass, component, config)
# Configure node
name = "{}.{}".format(component, _object_id(value))
@ -261,14 +242,10 @@ def setup(hass, config):
else:
value.disable_poll()
# Fire discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: discovery_service,
ATTR_DISCOVERED: {
ATTR_NODE_ID: node.node_id,
ATTR_VALUE_ID: value.value_id,
}
})
discovery.load_platform(hass, component, DOMAIN, {
ATTR_NODE_ID: node.node_id,
ATTR_VALUE_ID: value.value_id,
}, config)
def scene_activated(node, scene_id):
"""Called when a scene is activated on any node in the network."""

View File

@ -1,7 +1,7 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
__version__ = "0.21.2"
__version__ = "0.22.0"
REQUIRED_PYTHON_VER = (3, 4)
PLATFORM_FORMAT = '{}.{}'

View File

@ -73,12 +73,13 @@ def entity_id(value):
value = string(value).lower()
if valid_entity_id(value):
return value
raise vol.Invalid('Entity ID {} does not match format <domain>.<object_id>'
.format(value))
raise vol.Invalid('Entity ID {} is an invalid entity id'.format(value))
def entity_ids(value):
"""Validate Entity IDs."""
if value is None:
raise vol.Invalid('Entity IDs can not be None')
if isinstance(value, str):
value = [ent_id.strip() for ent_id in value.split(',')]

View File

@ -0,0 +1,86 @@
"""Helper methods to help with platform discovery."""
from homeassistant import bootstrap
from homeassistant.const import (
ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED)
EVENT_LOAD_PLATFORM = 'load_platform.{}'
ATTR_PLATFORM = 'platform'
def listen(hass, service, callback):
"""Setup listener for discovery of specific service.
Service can be a string or a list/tuple.
"""
if isinstance(service, str):
service = (service,)
else:
service = tuple(service)
def discovery_event_listener(event):
"""Listen for discovery events."""
if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service:
callback(event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED))
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener)
def discover(hass, service, discovered=None, component=None, hass_config=None):
"""Fire discovery event. Can ensure a component is loaded."""
if component is not None:
bootstrap.setup_component(hass, component, hass_config)
data = {
ATTR_SERVICE: service
}
if discovered is not None:
data[ATTR_DISCOVERED] = discovered
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)
def listen_platform(hass, component, callback):
"""Register a platform loader listener."""
service = EVENT_LOAD_PLATFORM.format(component)
def discovery_platform_listener(event):
"""Listen for platform discovery events."""
if event.data.get(ATTR_SERVICE) != service:
return
platform = event.data.get(ATTR_PLATFORM)
if not platform:
return
callback(platform, event.data.get(ATTR_DISCOVERED))
hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_platform_listener)
def load_platform(hass, component, platform, discovered=None,
hass_config=None):
"""Load a component and platform dynamically.
Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be
fired to load the platform. The event will contain:
{ ATTR_SERVICE = LOAD_PLATFORM + '.' + <<component>>
ATTR_PLATFORM = <<platform>>
ATTR_DISCOVERED = <<discovery info>> }
Use `listen_platform` to register a callback for these events.
"""
if component is not None:
bootstrap.setup_component(hass, component, hass_config)
data = {
ATTR_SERVICE: EVENT_LOAD_PLATFORM.format(component),
ATTR_PLATFORM: platform,
}
if discovered is not None:
data[ATTR_DISCOVERED] = discovered
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data)

View File

@ -2,11 +2,11 @@
from threading import Lock
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.components import discovery, group
from homeassistant.components import group
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE,
DEVICE_DEFAULT_NAME)
from homeassistant.helpers import config_per_platform
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.helpers.service import extract_entity_ids
@ -20,8 +20,7 @@ class EntityComponent(object):
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments
def __init__(self, logger, domain, hass,
scan_interval=DEFAULT_SCAN_INTERVAL,
discovery_platforms=None, group_name=None):
scan_interval=DEFAULT_SCAN_INTERVAL, group_name=None):
"""Initialize an entity component."""
self.logger = logger
self.hass = hass
@ -29,7 +28,6 @@ class EntityComponent(object):
self.domain = domain
self.entity_id_format = domain + '.{}'
self.scan_interval = scan_interval
self.discovery_platforms = discovery_platforms
self.group_name = group_name
self.entities = {}
@ -54,23 +52,14 @@ class EntityComponent(object):
for p_type, p_config in config_per_platform(config, self.domain):
self._setup_platform(p_type, p_config)
if self.discovery_platforms:
# Discovery listener for all items in discovery_platforms array
# passed from a component's setup method (e.g. light/__init__.py)
discovery.listen(
self.hass, self.discovery_platforms.keys(),
lambda service, info:
self._setup_platform(self.discovery_platforms[service], {},
info))
# Generic discovery listener for loading platform dynamically
# Refer to: homeassistant.components.discovery.load_platform()
def load_platform_callback(service, info):
def component_platform_discovered(platform, info):
"""Callback to load a platform."""
platform = info.pop(discovery.LOAD_PLATFORM)
self._setup_platform(platform, {}, info if info else None)
discovery.listen(self.hass, discovery.LOAD_PLATFORM + '.' +
self.domain, load_platform_callback)
self._setup_platform(platform, {}, info)
discovery.listen_platform(self.hass, self.domain,
component_platform_discovered)
def extract_from_service(self, service):
"""Extract all known entities from a service call.

View File

@ -17,8 +17,8 @@ def track_state_change(hass, entity_ids, action, from_state=None,
Returns the listener that listens on the bus for EVENT_STATE_CHANGED.
Pass the return value into hass.bus.remove_listener to remove it.
"""
from_state = _process_match_param(from_state)
to_state = _process_match_param(to_state)
from_state = _process_state_match(from_state)
to_state = _process_state_match(to_state)
# Ensure it is a lowercase list with entity ids we want to match on
if entity_ids == MATCH_ALL:
@ -155,7 +155,7 @@ def track_utc_time_change(hass, action, year=None, month=None, day=None,
hass.bus.listen(EVENT_TIME_CHANGED, time_change_listener)
return time_change_listener
pmp = _process_match_param
pmp = _process_time_match
year, month, day = pmp(year), pmp(month), pmp(day)
hour, minute, second = pmp(hour), pmp(minute), pmp(second)
@ -190,7 +190,17 @@ def track_time_change(hass, action, year=None, month=None, day=None,
second, local=True)
def _process_match_param(parameter):
def _process_state_match(parameter):
"""Wrap parameter in a tuple if it is not one and returns it."""
if parameter is None or parameter == MATCH_ALL:
return MATCH_ALL
elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'):
return (parameter,)
else:
return tuple(parameter)
def _process_time_match(parameter):
"""Wrap parameter in a tuple if it is not one and returns it."""
if parameter is None or parameter == MATCH_ALL:
return MATCH_ALL

View File

@ -12,9 +12,13 @@ from homeassistant.components.notify import (
ATTR_MESSAGE, SERVICE_NOTIFY)
from homeassistant.components.sun import (
STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON)
from homeassistant.components.switch.mysensors import (
ATTR_IR_CODE, SERVICE_SEND_IR_CODE)
from homeassistant.components.thermostat import (
ATTR_AWAY_MODE, ATTR_FAN, SERVICE_SET_AWAY_MODE, SERVICE_SET_FAN_MODE,
SERVICE_SET_TEMPERATURE)
from homeassistant.components.thermostat.ecobee import (
ATTR_FAN_MIN_ON_TIME, SERVICE_SET_FAN_MIN_ON_TIME)
from homeassistant.components.hvac import (
ATTR_HUMIDITY, ATTR_SWING_MODE, ATTR_OPERATION_MODE, ATTR_AUX_HEAT,
SERVICE_SET_HUMIDITY, SERVICE_SET_SWING_MODE,
@ -46,12 +50,14 @@ SERVICE_ATTRIBUTES = {
SERVICE_NOTIFY: [ATTR_MESSAGE],
SERVICE_SET_AWAY_MODE: [ATTR_AWAY_MODE],
SERVICE_SET_FAN_MODE: [ATTR_FAN],
SERVICE_SET_FAN_MIN_ON_TIME: [ATTR_FAN_MIN_ON_TIME],
SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE],
SERVICE_SET_HUMIDITY: [ATTR_HUMIDITY],
SERVICE_SET_SWING_MODE: [ATTR_SWING_MODE],
SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION_MODE],
SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT],
SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE],
SERVICE_SEND_IR_CODE: [ATTR_IR_CODE]
}
# Update this dict when new services are added to HA.

View File

@ -1,10 +1,35 @@
"""Color util methods."""
import logging
import math
# pylint: disable=unused-import
from webcolors import html5_parse_legacy_color as color_name_to_rgb # noqa
_LOGGER = logging.getLogger(__name__)
HASS_COLOR_MAX = 500 # mireds (inverted)
HASS_COLOR_MIN = 154
COLORS = {
'white': (255, 255, 255), 'beige': (245, 245, 220),
'tan': (210, 180, 140), 'gray': (128, 128, 128),
'navy blue': (0, 0, 128), 'royal blue': (8, 76, 158),
'blue': (0, 0, 255), 'azure': (0, 127, 255), 'aqua': (127, 255, 212),
'teal': (0, 128, 128), 'green': (0, 255, 0),
'forest green': (34, 139, 34), 'olive': (128, 128, 0),
'chartreuse': (127, 255, 0), 'lime': (191, 255, 0),
'golden': (255, 215, 0), 'red': (255, 0, 0), 'coral': (0, 63, 72),
'hot pink': (252, 15, 192), 'fuchsia': (255, 119, 255),
'lavender': (181, 126, 220), 'indigo': (75, 0, 130),
'maroon': (128, 0, 0), 'crimson': (220, 20, 60)}
def color_name_to_rgb(color_name):
"""Convert color name to RGB hex value."""
hex_value = COLORS.get(color_name.lower())
if not hex_value:
_LOGGER.error('unknown color supplied %s default to white', color_name)
hex_value = COLORS['white']
return hex_value
# Taken from:

View File

@ -5,7 +5,6 @@ pytz>=2016.4
pip>=7.0.0
jinja2>=2.8
voluptuous==0.8.9
webcolors==1.5
eventlet==0.19.0
# homeassistant.components.isy994
@ -30,7 +29,10 @@ Werkzeug==0.11.5
apcaccess==0.0.4
# homeassistant.components.sun
astral==1.1
astral==1.2
# homeassistant.components.sensor.swiss_hydrological_data
beautifulsoup4==4.4.1
# homeassistant.components.light.blinksticklight
blinkstick==1.1.7
@ -95,9 +97,6 @@ hikvision==0.4
# homeassistant.components.sensor.dht
# http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0
# homeassistant.components.sensor.netatmo
https://github.com/HydrelioxGitHub/netatmo-api-python/archive/43ff238a0122b0939a0dc4e8836b6782913fb6e2.zip#lnetatmo==0.4.0
# homeassistant.components.switch.dlink
https://github.com/LinuxChristian/pyW215/archive/v0.1.1.zip#pyW215==0.1.1
@ -124,6 +123,9 @@ https://github.com/danieljkemp/onkyo-eiscp/archive/python3.zip#onkyo-eiscp==0.9.
# homeassistant.components.device_tracker.fritz
# https://github.com/deisi/fritzconnection/archive/b5c14515e1c8e2652b06b6316a7f3913df942841.zip#fritzconnection==0.4.6
# homeassistant.components.netatmo
https://github.com/jabesq/netatmo-api-python/archive/v0.5.0.zip#lnetatmo==0.5.0
# homeassistant.components.sensor.sabnzbd
https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1
@ -131,7 +133,7 @@ https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad591540925
https://github.com/kellerza/pyqwikswitch/archive/v0.4.zip#pyqwikswitch==0.4
# homeassistant.components.ecobee
https://github.com/nkgilley/python-ecobee-api/archive/4a884bc146a93991b4210f868f3d6aecf0a181e6.zip#python-ecobee==0.0.5
https://github.com/nkgilley/python-ecobee-api/archive/4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6
# homeassistant.components.switch.edimax
https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f3700.zip#pyedimax==0.1
@ -172,6 +174,9 @@ lightify==1.0.3
# homeassistant.components.light.limitlessled
limitlessled==1.0.0
# homeassistant.components.sensor.swiss_hydrological_data
lxml==3.6.0
# homeassistant.components.notify.message_bird
messagebird==1.2.0
@ -196,12 +201,14 @@ panasonic_viera==0.2
# homeassistant.components.device_tracker.aruba
# homeassistant.components.device_tracker.asuswrt
# homeassistant.components.media_player.pandora
pexpect==4.0.1
# homeassistant.components.light.hue
phue==0.8
# homeassistant.components.media_player.plex
# homeassistant.components.sensor.plex
plexapi==1.1.0
# homeassistant.components.thermostat.proliphix
@ -252,6 +259,9 @@ pyloopenergy==0.0.13
# homeassistant.components.device_tracker.netgear
pynetgear==0.3.3
# homeassistant.components.switch.netio
pynetio==0.1.6
# homeassistant.components.alarm_control_panel.nx584
# homeassistant.components.binary_sensor.nx584
pynx584==0.2
@ -263,6 +273,7 @@ pyowm==2.3.1
pyserial<=3.0
# homeassistant.components.device_tracker.snmp
# homeassistant.components.sensor.snmp
pysnmp==4.3.2
# homeassistant.components.sensor.forecast
@ -287,7 +298,7 @@ python-pushover==0.2
python-statsd==1.7.2
# homeassistant.components.notify.telegram
python-telegram-bot==4.2.0
python-telegram-bot==4.2.1
# homeassistant.components.sensor.twitch
python-twitch==1.2.0
@ -297,9 +308,10 @@ python-twitch==1.2.0
# homeassistant.components.garage_door.wink
# homeassistant.components.light.wink
# homeassistant.components.lock.wink
# homeassistant.components.rollershutter.wink
# homeassistant.components.sensor.wink
# homeassistant.components.switch.wink
python-wink==0.7.6
python-wink==0.7.7
# homeassistant.components.keyboard
pyuserinput==0.1.9

View File

@ -5,5 +5,6 @@ pytest>=2.9.1
pytest-cov>=2.2.0
pytest-timeout>=1.0.0
pytest-capturelog>=0.7
betamax==0.5.1
betamax==0.7.0
pydocstyle>=1.0.0
httpretty==0.8.14

View File

@ -17,7 +17,6 @@ REQUIRES = [
'pip>=7.0.0',
'jinja2>=2.8',
'voluptuous==0.8.9',
'webcolors==1.5',
'eventlet==0.19.0',
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,69 @@
"""The tests for the ASUSWRT device tracker platform."""
import os
import unittest
from unittest import mock
from homeassistant.components import device_tracker
from homeassistant.const import (CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME,
CONF_HOST)
from tests.common import get_test_home_assistant
class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase):
"""Tests for the ASUSWRT device tracker platform."""
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
def tearDown(self): # pylint: disable=invalid-name
"""Stop everything that was started."""
try:
os.remove(self.hass.config.path(device_tracker.YAML_DEVICES))
except FileNotFoundError:
pass
def test_password_or_pub_key_required(self):
"""Test creating an AsusWRT scanner without a pass or pubkey."""
self.assertIsNone(device_tracker.asuswrt.get_scanner(
self.hass, {device_tracker.DOMAIN: {
CONF_PLATFORM: 'asuswrt',
CONF_HOST: 'fake_host',
CONF_USERNAME: 'fake_user'
}}))
@mock.patch(
'homeassistant.components.device_tracker.asuswrt.AsusWrtDeviceScanner',
return_value=mock.MagicMock())
def test_get_scanner_with_password_no_pubkey(self, asuswrt_mock):
"""Test creating an AsusWRT scanner with a password and no pubkey."""
conf_dict = {
device_tracker.DOMAIN: {
CONF_PLATFORM: 'asuswrt',
CONF_HOST: 'fake_host',
CONF_USERNAME: 'fake_user',
CONF_PASSWORD: 'fake_pass'
}
}
self.assertIsNotNone(device_tracker.asuswrt.get_scanner(
self.hass, conf_dict))
asuswrt_mock.assert_called_once_with(conf_dict[device_tracker.DOMAIN])
@mock.patch(
'homeassistant.components.device_tracker.asuswrt.AsusWrtDeviceScanner',
return_value=mock.MagicMock())
def test_get_scanner_with_pubkey_no_password(self, asuswrt_mock):
"""Test creating an AsusWRT scanner with a pubkey and no password."""
conf_dict = {
device_tracker.DOMAIN: {
CONF_PLATFORM: 'asuswrt',
CONF_HOST: 'fake_host',
CONF_USERNAME: 'fake_user',
'pub_key': '/fake_path'
}
}
self.assertIsNotNone(device_tracker.asuswrt.get_scanner(
self.hass, conf_dict))
asuswrt_mock.assert_called_once_with(conf_dict[device_tracker.DOMAIN])

View File

@ -0,0 +1,53 @@
"""The tests for the BT Home Hub 5 device tracker platform."""
import unittest
from unittest.mock import patch
from homeassistant.components.device_tracker import bt_home_hub_5
from homeassistant.const import CONF_HOST
patch_file = 'homeassistant.components.device_tracker.bt_home_hub_5'
def _get_homehub_data(url):
"""Return mock homehub data."""
return '''
[
{
"mac": "AA:BB:CC:DD:EE:FF,
"hostname": "hostname",
"ip": "192.168.1.43",
"ipv6": "",
"name": "hostname",
"activity": "1",
"os": "Unknown",
"device": "Unknown",
"time_first_seen": "2016/06/05 11:14:45",
"time_last_active": "2016/06/06 11:33:08",
"dhcp_option": "39043T90430T9TGK0EKGE5KGE3K904390K45GK054",
"port": "wl0",
"ipv6_ll": "fe80::gd67:ghrr:fuud:4332",
"activity_ip": "1",
"activity_ipv6_ll": "0",
"activity_ipv6": "0",
"device_oui": "NA",
"device_serial": "NA",
"device_class": "NA"
}
]
'''
class TestBTHomeHub5DeviceTracker(unittest.TestCase):
"""Test BT Home Hub 5 device tracker platform."""
@patch('{}._get_homehub_data'.format(patch_file), new=_get_homehub_data)
def test_config_minimal(self):
"""Test the setup with minimal configuration."""
config = {
'device_tracker': {
CONF_HOST: 'foo'
}
}
result = bt_home_hub_5.get_scanner(None, config)
self.assertIsNotNone(result)

View File

@ -25,7 +25,7 @@ class TestSensorYr:
def test_default_setup(self, betamax_session):
"""Test the default setup."""
now = datetime(2016, 1, 5, 1, tzinfo=dt_util.UTC)
now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.sensor.yr.requests.Session',
return_value=betamax_session):
@ -37,13 +37,13 @@ class TestSensorYr:
state = self.hass.states.get('sensor.yr_symbol')
assert '46' == state.state
assert '3' == state.state
assert state.state.isnumeric()
assert state.attributes.get('unit_of_measurement') is None
def test_custom_setup(self, betamax_session):
"""Test a custom setup."""
now = datetime(2016, 1, 5, 1, tzinfo=dt_util.UTC)
now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.sensor.yr.requests.Session',
return_value=betamax_session):
@ -61,15 +61,15 @@ class TestSensorYr:
state = self.hass.states.get('sensor.yr_pressure')
assert 'hPa' == state.attributes.get('unit_of_measurement')
assert '1025.1' == state.state
assert '1009.3' == state.state
state = self.hass.states.get('sensor.yr_wind_direction')
assert '°' == state.attributes.get('unit_of_measurement')
assert '81.8' == state.state
assert '103.6' == state.state
state = self.hass.states.get('sensor.yr_humidity')
assert '%' == state.attributes.get('unit_of_measurement')
assert '79.6' == state.state
assert '55.5' == state.state
state = self.hass.states.get('sensor.yr_fog')
assert '%' == state.attributes.get('unit_of_measurement')
@ -77,4 +77,4 @@ class TestSensorYr:
state = self.hass.states.get('sensor.yr_wind_speed')
assert 'm/s', state.attributes.get('unit_of_measurement')
assert '4.3' == state.state
assert '3.5' == state.state

View File

@ -3,7 +3,7 @@
import unittest
import homeassistant.components.configurator as configurator
from homeassistant.const import EVENT_TIME_CHANGED
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME
from tests.common import get_test_home_assistant
@ -40,26 +40,25 @@ class TestConfigurator(unittest.TestCase):
def test_request_all_info(self):
"""Test request config with all possible info."""
values = [
"config_description", "config image url",
"config submit caption", []]
keys = [
configurator.ATTR_DESCRIPTION, configurator.ATTR_DESCRIPTION_IMAGE,
configurator.ATTR_SUBMIT_CAPTION, configurator.ATTR_FIELDS]
exp_attr = dict(zip(keys, values))
exp_attr[configurator.ATTR_CONFIGURE_ID] = configurator.request_config(
self.hass, "Test Request", lambda _: None,
*values)
exp_attr = {
ATTR_FRIENDLY_NAME: "Test Request",
configurator.ATTR_DESCRIPTION: "config description",
configurator.ATTR_DESCRIPTION_IMAGE: "config image url",
configurator.ATTR_SUBMIT_CAPTION: "config submit caption",
configurator.ATTR_FIELDS: [],
configurator.ATTR_CONFIGURE_ID: configurator.request_config(
self.hass, "Test Request", lambda _: None,
"config description", "config image url",
"config submit caption"
)
}
states = self.hass.states.all()
self.assertEqual(1, len(states))
state = states[0]
self.assertEqual(configurator.STATE_CONFIGURE, state.state)
self.assertEqual(exp_attr, state.attributes)
assert exp_attr == dict(state.attributes)
def test_callback_called_on_configure(self):
"""Test if our callback gets called when configure service called."""

View File

@ -0,0 +1,76 @@
"""The tests for the forecast.io platform."""
import json
import re
import os
import unittest
from unittest.mock import MagicMock, patch
import forecastio
import httpretty
from requests.exceptions import HTTPError
from homeassistant.components.sensor import forecast
from homeassistant import core as ha
class TestForecastSetup(unittest.TestCase):
"""Test the forecast.io platform."""
def setUp(self):
"""Initialize values for this testcase class."""
self.hass = ha.HomeAssistant()
self.key = 'foo'
self.config = {
'api_key': 'foo',
'monitored_conditions': ['summary', 'icon']
}
self.lat = 37.8267
self.lon = -122.423
self.hass.config.latitude = self.lat
self.hass.config.longitude = self.lon
def test_setup_no_latitude(self):
"""Test that the component is not loaded without required config."""
self.hass.config.latitude = None
self.assertFalse(forecast.setup_platform(self.hass, {}, MagicMock()))
@patch('forecastio.api.get_forecast')
def test_setup_bad_api_key(self, mock_get_forecast):
"""Test for handling a bad API key."""
# The forecast API wrapper that we use raises an HTTP error
# when you try to use a bad (or no) API key.
url = 'https://api.forecast.io/forecast/{}/{},{}?units=auto'.format(
self.key, str(self.lat), str(self.lon)
)
msg = '400 Client Error: Bad Request for url: {}'.format(url)
mock_get_forecast.side_effect = HTTPError(msg,)
response = forecast.setup_platform(self.hass, self.config, MagicMock())
self.assertFalse(response)
@httpretty.activate
@patch('forecastio.api.get_forecast', wraps=forecastio.api.get_forecast)
def test_setup(self, mock_get_forecast):
"""Test for successfully setting up the forecast.io platform."""
def load_fixture_from_json():
cwd = os.path.dirname(__file__)
fixture_path = os.path.join(cwd, '..', 'fixtures', 'forecast.json')
with open(fixture_path) as file:
content = json.load(file)
return json.dumps(content)
# Mock out any calls to the actual API and
# return the fixture json instead
uri = 'api.forecast.io\/forecast\/(\w+)\/(-?\d+\.?\d*),(-?\d+\.?\d*)'
httpretty.register_uri(
httpretty.GET,
re.compile(uri),
body=load_fixture_from_json(),
)
# The following will raise an error if the regex for the mock was
# incorrect and we actually try to go out to the internet.
httpretty.HTTPretty.allow_net_connect = False
forecast.setup_platform(self.hass, self.config, MagicMock())
self.assertTrue(mock_get_forecast.called)
self.assertEqual(mock_get_forecast.call_count, 1)

View File

@ -51,6 +51,30 @@ class TestShellCommand(unittest.TestCase):
}
})
def test_template_render_no_template(self):
"""Ensure shell_commands without templates get rendered properly."""
cmd, shell = shell_command._parse_command(self.hass, 'ls /bin', {})
self.assertTrue(shell)
self.assertEqual(cmd, 'ls /bin')
def test_template_render(self):
"""Ensure shell_commands with templates get rendered properly."""
self.hass.states.set('sensor.test_state', 'Works')
cmd, shell = shell_command._parse_command(
self.hass,
'ls /bin {{ states.sensor.test_state.state }}', {}
)
self.assertFalse(shell, False)
self.assertEqual(cmd[-1], 'Works')
def test_invalid_template_fails(self):
"""Test that shell_commands with invalid templates fail."""
cmd, _shell = shell_command._parse_command(
self.hass,
'ls /bin {{ states. .test_state.state }}', {}
)
self.assertEqual(cmd, None)
@patch('homeassistant.components.shell_command.subprocess.call',
side_effect=SubprocessError)
@patch('homeassistant.components.shell_command._LOGGER.error')

1462
tests/fixtures/forecast.json vendored Normal file

File diff suppressed because it is too large Load Diff

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