Merge pull request #2509 from home-assistant/dev

0.24
This commit is contained in:
Fabian Affolter 2016-07-17 00:03:26 +02:00 committed by GitHub
commit dc0f16c9dd
133 changed files with 3432 additions and 1453 deletions

View File

@ -3,6 +3,7 @@ source = homeassistant
omit =
homeassistant/__main__.py
homeassistant/scripts/*.py
# omit pieces of code that rely on external devices being present
homeassistant/components/apcupsd.py
@ -87,8 +88,13 @@ omit =
homeassistant/components/homematic.py
homeassistant/components/*/homematic.py
homeassistant/components/knx.py
homeassistant/components/switch/knx.py
homeassistant/components/binary_sensor/knx.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/nx584.py
homeassistant/components/alarm_control_panel/simplisafe.py
homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py
@ -120,6 +126,7 @@ omit =
homeassistant/components/garage_door/rpi_gpio.py
homeassistant/components/hdmi_cec.py
homeassistant/components/ifttt.py
homeassistant/components/joaoapps_join.py
homeassistant/components/keyboard.py
homeassistant/components/light/blinksticklight.py
homeassistant/components/light/hue.py
@ -156,6 +163,7 @@ omit =
homeassistant/components/notify/gntp.py
homeassistant/components/notify/googlevoice.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/joaoapps_join.py
homeassistant/components/notify/message_bird.py
homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py
@ -185,6 +193,7 @@ omit =
homeassistant/components/sensor/glances.py
homeassistant/components/sensor/google_travel_time.py
homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/neurio_energy.py
@ -209,6 +218,7 @@ omit =
homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py
homeassistant/components/sensor/worldclock.py
homeassistant/components/sensor/yweather.py
homeassistant/components/switch/acer_projector.py
homeassistant/components/switch/arest.py
homeassistant/components/switch/dlink.py
@ -220,6 +230,7 @@ omit =
homeassistant/components/switch/pulseaudio_loopback.py
homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/wake_on_lan.py
homeassistant/components/thermostat/eq3btsmart.py

View File

@ -15,7 +15,7 @@
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io)
If code communicates with devices:
If code communicates with devices, web services, or a:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
- [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]).
- [ ] New dependencies are only imported inside functions that use them ([example][ex-import]).
@ -26,8 +26,5 @@ If the code does not interact with devices:
- [ ] Local tests with `tox` run successfully. **Your PR cannot be merged unless tests pass**
- [ ] Tests have been added to verify that the new code works.
[fork]: http://stackoverflow.com/a/7244456
[squash]: https://github.com/ginatrapani/todo.txt-android/wiki/Squash-All-Commits-Related-to-a-Single-Issue-into-a-Single-Commit
[ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L16
[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard.py#L51

View File

@ -9,79 +9,5 @@ The process is straight-forward.
- Ensure tests work.
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
Still interested? Then you should read the next sections and get more details.
Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.
## Adding support for a new device
For help on building your component, please see the [developer documentation](https://home-assistant.io/developers/) on [home-assistant.io](https://home-assistant.io/).
After you finish adding support for your device:
- Check that all dependencies are included via the `REQUIREMENTS` variable in your platform/component and only imported inside functions that use them.
- Add any new dependencies to `requirements_all.txt` if needed. Use `script/gen_requirements_all.py`.
- Update the `.coveragerc` file to exclude your platform if there are no tests available or your new code uses a 3rd party library for communication with the device/service/sensor.
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). It's OK to just add a docstring with configuration details (sample entry for `configuration.yaml` file and alike) to the file header as a start. Visit the [website documentation](https://home-assistant.io/developers/website/) for further information on contributing to [home-assistant.io](https://github.com/home-assistant/home-assistant.io).
- Make sure all your code passes ``pylint`` and ``flake8`` (PEP8 and some more) validation. To check your repository, run `tox` or `script/lint`.
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
- Check for comments and suggestions on your Pull Request and keep an eye on the [CI output](https://travis-ci.org/home-assistant/home-assistant/).
If you add a platform for an existing component, there is usually no need for updating the frontend. Only if you've added a new component that should show up in the frontend, there are more steps needed:
- Update the file [`home-assistant-icons.html`](https://github.com/home-assistant/home-assistant/blob/master/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html) with an icon for your domain ([pick one from this list](https://www.polymer-project.org/1.0/components/core-elements/demo.html#core-icon)).
- Update the demo component with two states that it provides.
- Add your component to `home-assistant.conf.example`.
Since you've updated `home-assistant-icons.html`, you've made changes to the frontend:
- Run `script/build_frontend`. This will build a new version of the frontend. Make sure you add the changed files `frontend.py` and `frontend.html` to the commit.
### Setting states
It is the responsibility of the component to maintain the states of the devices in your domain. Each device should be a single state and, if possible, a group should be provided that tracks the combined state of the devices.
A state can have several attributes that will help the frontend in displaying your state:
- `friendly_name`: this name will be used as the name of the device
- `entity_picture`: this picture will be shown instead of the domain icon
- `unit_of_measurement`: this will be appended to the state in the interface
- `hidden`: This is a suggestion to the frontend on if the state should be hidden
These attributes are defined in [homeassistant.components](https://github.com/home-assistant/home-assistant/blob/master/homeassistant/components/__init__.py#L25).
### Proper Visibility Handling
Generally, when creating a new entity for Home Assistant you will want it to be a class that inherits the [homeassistant.helpers.entity.Entity](https://github.com/home-assistant/home-assistant/blob/master/homeassistant/helpers/entity.py) class. If this is done, visibility will be handled for you.
You can set a suggestion for your entity's visibility by setting the hidden property by doing something similar to the following.
```python
self.hidden = True
```
This will SUGGEST that the active frontend hides the entity. This requires that the active frontend support hidden cards (the default frontend does) and that the value of hidden be included in your attributes dictionary (see above). The Entity abstract class will take care of this for you.
Remember: The suggestion set by your component's code will always be overwritten by user settings in the configuration.yaml file. This is why you may set hidden to be False, but the property may remain True (or vice-versa).
### Working on the frontend
The frontend is composed of [Polymer](https://www.polymer-project.org) web-components and compiled into the file `frontend.html`. During development you do not want to work with the compiled version but with the seperate files. To have Home Assistant serve the seperate files, set `development=1` for the *http-component* in your config.
When you are done with development and ready to commit your changes, run `build_frontend`, set `development=0` in your config and validate that everything still works.
## Testing your code
To test your code before submission, used the `tox` tool.
```bash
> pip install -U tox
> tox
```
This will run unit tests against python 3.4 and 3.5 (if both are available locally), as well as run a set of tests which validate `pep8` and `pylint` style of the code.
You can optionally run tests on only one tox target using the `-e` option to select an environment.
For instance `tox -e lint` will run the linters only, `tox -e py34` will run unit tests only on python 3.4.
### Notes on PyLint and PEP8 validation
In case a PyLint warning cannot be avoided, add a comment to disable the PyLint check for that line. This can be done using the format `# pylint: disable=YOUR-ERROR-NAME`. Example of an unavoidable PyLint warning is if you do not use the passed in datetime if you're listening for time change.

View File

@ -18,7 +18,7 @@ tutorials and documentation.
|screenshot-states|
Examples of devices it can interface it:
Examples of devices Home Assistant can interface with:
- Monitoring connected devices to a wireless router:
`OpenWrt <https://openwrt.org/>`__,
@ -61,11 +61,11 @@ Examples of devices it can interface it:
- `See full list of supported
devices <https://home-assistant.io/components/>`__
Built home automation on top of your devices:
Build home automation on top of your devices:
- Keep a precise history of every change to the state of your house
- Turn on the lights when people get home after sun set
- Turn on lights slowly during sun set to compensate for less light
- Turn on the lights when people get home after sunset
- Turn on lights slowly during sunset to compensate for less light
- Turn off all lights and devices when everybody leaves the house
- Offers a `REST API <https://home-assistant.io/developers/api/>`__
and can interface with MQTT for easy integration with other projects
@ -75,10 +75,10 @@ Built home automation on top of your devices:
(NMA) <http://www.notifymyandroid.com/>`__,
`PushBullet <https://www.pushbullet.com/>`__,
`PushOver <https://pushover.net/>`__, `Slack <https://slack.com/>`__,
`Telegram <https://telegram.org/>`__, and `Jabber
`Telegram <https://telegram.org/>`__, `Join <http://joaoapps.com/join/>`__, and `Jabber
(XMPP) <http://xmpp.org>`__
The system is built modular so support for other devices or actions can
The system is built using a modular approach so support for other devices or actions can
be implemented easily. See also the `section on
architecture <https://home-assistant.io/developers/architecture/>`__
and the `section on creating your own

View File

@ -7,6 +7,9 @@ homeassistant:
latitude: 32.87336
longitude: 117.22743
# Impacts weather/sunrise data
elevation: 665
# C for Celsius, F for Fahrenheit
temperature_unit: C
@ -22,8 +25,8 @@ http:
# Set to 1 to enable development mode
# development: 1
# Enable the frontend
frontend:
# enable the frontend
light:
# platform: hue
@ -33,17 +36,12 @@ wink:
access_token: 'YOUR_TOKEN'
device_tracker:
# The following types are available: ddwrt, netgear, tomato, luci,
# and nmap_tracker
# The following tracker are available:
# https://home-assistant.io/components/#presence-detection
platform: netgear
host: 192.168.1.1
username: admin
password: PASSWORD
# http_id is needed for Tomato routers only
# http_id: ABCDEFGHH
# For nmap_tracker, only the IP addresses to scan are needed:
# hosts: 192.168.1.1/24 # netmask prefix notation or
# hosts: 192.168.1.1-255 # address range
chromecast:
@ -74,24 +72,25 @@ device_sun_light_trigger:
# A comma separated list of states that have to be tracked as a single group
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
# You can also have groups within groups.
# https://home-assistant.io/components/group/
group:
Home:
- group.living_room
- group.kitchen
living_room:
- light.Bowl
- light.Ceiling
- light.TV_back_light
kitchen:
- light.fan_bulb_1
- light.fan_bulb_2
children:
- device_tracker.child_1
- device_tracker.child_2
default_view:
view: yes
entities:
- group.awesome_people
- group.climate
process:
# items are which processes to look for: <entity_id>: <search string within ps>
xbmc: XBMC.App
kitchen:
name: Kitchen
entities:
- switch.kitchen_pin_3
upstairs:
name: Kids
icon: mdi:account-multiple
view: yes
entities:
- input_boolean.notify_home
- camera.demo_camera
example:
@ -105,6 +104,7 @@ browser:
keyboard:
# https://home-assistant.io/getting-started/automation/
automation:
- alias: 'Rule 1 Light on in the evening'
trigger:
@ -126,7 +126,6 @@ automation:
entity_id: group.living_room
- alias: 'Rule 2 - Away Mode'
trigger:
- platform: state
entity_id: group.all_devices
@ -139,6 +138,14 @@ automation:
# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc.
# Each sensor label should be unique or your sensors might not load correctly.
# Another way to do is to collect all entries under one "sensor:"
# sensor:
# - platform: mqtt
# name: "MQTT Sensor 1"
# - platform: mqtt
# name: "MQTT Sensor 2"
#
# Details: https://home-assistant.io/getting-started/devices/
sensor:
platform: systemmonitor
@ -149,14 +156,6 @@ sensor:
arg: '/home'
- type: 'disk_use'
arg: '/home'
- type: 'disk_free'
arg: '/'
- type: 'memory_use_percent'
- type: 'memory_use'
- type: 'memory_free'
- type: 'processor_use'
- type: 'process'
arg: 'octave-cli'
sensor 2:
platform: forecast
@ -166,14 +165,6 @@ sensor 2:
- precip_type
- precip_intensity
- temperature
- dew_point
- wind_speed
- wind_bearing
- cloud_cover
- humidity
- pressure
- visibility
- ozone
script:
# Turns on the bedroom lights and then the living room lights 1 minute later

View File

@ -7,7 +7,6 @@ import platform
import subprocess
import sys
import threading
import time
from homeassistant.const import (
__version__,
@ -110,22 +109,14 @@ def get_arguments():
type=int,
default=None,
help='Enables daily log rotation and keeps up to the specified days')
parser.add_argument(
'--install-osx',
action='store_true',
help='Installs as a service on OS X and loads on boot.')
parser.add_argument(
'--uninstall-osx',
action='store_true',
help='Uninstalls from OS X.')
parser.add_argument(
'--restart-osx',
action='store_true',
help='Restarts on OS X.')
parser.add_argument(
'--runner',
action='store_true',
help='On restart exit with code {}'.format(RESTART_EXIT_CODE))
parser.add_argument(
'--script',
nargs=argparse.REMAINDER,
help='Run one of the embedded scripts')
if os.name == "posix":
parser.add_argument(
'--daemon',
@ -196,46 +187,6 @@ def write_pid(pid_file):
sys.exit(1)
def install_osx():
"""Setup to run via launchd on OS X."""
with os.popen('which hass') as inp:
hass_path = inp.read().strip()
with os.popen('whoami') as inp:
user = inp.read().strip()
cwd = os.path.dirname(__file__)
template_path = os.path.join(cwd, 'startup', 'launchd.plist')
with open(template_path, 'r', encoding='utf-8') as inp:
plist = inp.read()
plist = plist.replace("$HASS_PATH$", hass_path)
plist = plist.replace("$USER$", user)
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
try:
with open(path, 'w', encoding='utf-8') as outp:
outp.write(plist)
except IOError as err:
print('Unable to write to ' + path, err)
return
os.popen('launchctl load -w -F ' + path)
print("Home Assistant has been installed. \
Open it here: http://localhost:8123")
def uninstall_osx():
"""Unload from launchd on OS X."""
path = os.path.expanduser("~/Library/LaunchAgents/org.homeassistant.plist")
os.popen('launchctl unload ' + path)
print("Home Assistant has been uninstalled.")
def closefds_osx(min_fd, max_fd):
"""Make sure file descriptors get closed when we restart.
@ -358,23 +309,13 @@ def main():
args = get_arguments()
if args.script is not None:
from homeassistant import scripts
return scripts.run(args.script)
config_dir = os.path.join(os.getcwd(), args.config)
ensure_config_path(config_dir)
# OS X launchd functions
if args.install_osx:
install_osx()
return 0
if args.uninstall_osx:
uninstall_osx()
return 0
if args.restart_osx:
uninstall_osx()
# A small delay is needed on some systems to let the unload finish.
time.sleep(0.5)
install_osx()
return 0
# Daemon functions
if args.pid_file:
check_pid(args.pid_file)

View File

@ -0,0 +1,124 @@
"""
Interfaces with SimpliSafe alarm control panel.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.simplisafe/
"""
import logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, STATE_UNKNOWN,
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['https://github.com/w1ll1am23/simplisafe-python/archive/'
'586fede0e85fd69e56e516aaa8e97eb644ca8866.zip#'
'simplisafe-python==0.0.1']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the SimpliSafe platform."""
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
if username is None or password is None:
_LOGGER.error('Must specify username and password!')
return False
add_devices([SimpliSafeAlarm(
config.get('name', "SimpliSafe"),
username,
password,
config.get('code'))])
# pylint: disable=abstract-method
class SimpliSafeAlarm(alarm.AlarmControlPanel):
"""Representation a SimpliSafe alarm."""
def __init__(self, name, username, password, code):
"""Initialize the SimpliSafe alarm."""
from simplisafe import SimpliSafe
self.simplisafe = SimpliSafe(username, password)
self._name = name
self._code = str(code) if code else None
self._id = self.simplisafe.get_id()
status = self.simplisafe.get_state()
if status == 'Off':
self._state = STATE_ALARM_DISARMED
elif status == 'Home':
self._state = STATE_ALARM_ARMED_HOME
elif status == 'Away':
self._state = STATE_ALARM_ARMED_AWAY
else:
self._state = STATE_UNKNOWN
@property
def should_poll(self):
"""Poll the SimpliSafe API."""
return True
@property
def name(self):
"""Return the name of the device."""
if self._name is not None:
return self._name
else:
return 'Alarm {}'.format(self._id)
@property
def code_format(self):
"""One or more characters if code is defined."""
return None if self._code is None else '.+'
@property
def state(self):
"""Return the state of the device."""
return self._state
def update(self):
"""Update alarm status."""
self.simplisafe.get_location()
status = self.simplisafe.get_state()
if status == 'Off':
self._state = STATE_ALARM_DISARMED
elif status == 'Home':
self._state = STATE_ALARM_ARMED_HOME
elif status == 'Away':
self._state = STATE_ALARM_ARMED_AWAY
else:
self._state = STATE_UNKNOWN
def alarm_disarm(self, code=None):
"""Send disarm command."""
if not self._validate_code(code, 'disarming'):
return
self.simplisafe.set_state('off')
_LOGGER.info('SimpliSafe alarm disarming')
self.update()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
if not self._validate_code(code, 'arming home'):
return
self.simplisafe.set_state('home')
_LOGGER.info('SimpliSafe alarm arming home')
self.update()
def alarm_arm_away(self, code=None):
"""Send arm away command."""
if not self._validate_code(code, 'arming away'):
return
self.simplisafe.set_state('away')
_LOGGER.info('SimpliSafe alarm arming away')
self.update()
def _validate_code(self, code, state):
"""Validate given code."""
check = self._code is None or code == self._code
if not check:
_LOGGER.warning('Wrong code entered for %s', state)
return check

View File

@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Perform the setup for Envisalink sensor devices."""
"""Setup Envisalink binary sensor devices."""
_configured_zones = discovery_info['zones']
for zone_num in _configured_zones:
_device_config_data = ZONE_SCHEMA(_configured_zones[zone_num])
@ -33,7 +33,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
"""Representation of an envisalink Binary Sensor."""
"""Representation of an Envisalink binary sensor."""
# pylint: disable=too-many-arguments
def __init__(self, zone_number, zone_name, zone_type, info, controller):

View File

@ -0,0 +1,24 @@
"""
Contains functionality to use a KNX group address as a binary.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.knx/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.knx import (
KNXConfig, KNXGroupAddress)
DEPENDENCIES = ["knx"]
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Setup the KNX binary sensor platform."""
add_entities([
KNXSwitch(hass, KNXConfig(config))
])
class KNXSwitch(KNXGroupAddress, BinarySensorDevice):
"""Representation of a KNX binary sensor device."""
pass

View File

@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup nx584 sensors."""
"""Setup nx584 binary sensor platform."""
from nx584 import client as nx584_client
host = config.get('host', 'localhost:5007')

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
at https://home-assistant.io/components/sensor.wink/
"""
import logging
import json
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.sensor.wink import WinkDevice
@ -12,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
# These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = {
@ -24,7 +25,7 @@ SENSOR_TYPES = {
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink platform."""
"""Setup the Wink binary sensor platform."""
import pywink
if discovery_info is None:
@ -42,9 +43,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if sensor.capability() in SENSOR_TYPES:
add_devices([WinkBinarySensorDevice(sensor)])
for key in pywink.get_keys():
add_devices([WinkBinarySensorDevice(key)])
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
"""Representation of a Wink sensor."""
"""Representation of a Wink binary sensor."""
def __init__(self, wink):
"""Initialize the Wink binary sensor."""
@ -53,6 +57,14 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
self._unit_of_measurement = self.wink.UNIT
self.capability = self.wink.capability()
def _pubnub_update(self, message, channel):
if 'data' in message:
json_data = json.dumps(message.get('data'))
else:
json_data = message
self.wink.pubnub_update(json.loads(json_data))
self.update_ha_state()
@property
def is_on(self):
"""Return true if the binary sensor is on."""

View File

@ -12,7 +12,7 @@ DEPENDENCIES = ["zigbee"]
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create and add an entity based on the configuration."""
"""Setup the ZigBee binary sensor platform."""
add_entities([
ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config))
])

View File

@ -8,6 +8,7 @@ import logging
import datetime
import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_point_in_time
from homeassistant.helpers.entity import Entity
from homeassistant.components import zwave
from homeassistant.components.binary_sensor import (
DOMAIN,
@ -31,7 +32,7 @@ DEVICE_MAPPINGS = {
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Z-Wave platform for sensors."""
"""Setup the Z-Wave platform for binary sensors."""
if discovery_info is None or zwave.NETWORK is None:
return
@ -61,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([ZWaveBinarySensor(value, None)])
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity):
"""Representation of a binary sensor within Z-Wave."""
def __init__(self, value, sensor_class):
@ -97,7 +98,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
self.update_ha_state()
class ZWaveTriggerSensor(ZWaveBinarySensor):
class ZWaveTriggerSensor(ZWaveBinarySensor, Entity):
"""Representation of a stateless sensor within Z-Wave."""
def __init__(self, sensor_value, sensor_class, hass, re_arm_sec=60):

View File

@ -18,7 +18,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DemoCamera(Camera):
"""A Demo camera."""
"""The representation of a Demo camera."""
def __init__(self, name):
"""Initialize demo camera component."""

View File

@ -89,7 +89,7 @@ class WelcomeData(object):
"""Return all module available on the API as a list."""
self.update()
if not self.home:
for home in self.welcomedata.cameras.keys():
for home in self.welcomedata.cameras:
for camera in self.welcomedata.cameras[home].values():
self.camera_names.append(camera['name'])
else:

View File

@ -27,7 +27,7 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REQUIREMENTS = ['fuzzywuzzy==0.10.0']
REQUIREMENTS = ['fuzzywuzzy==0.11.0']
def setup(hass, config):
@ -67,8 +67,8 @@ def setup(hass, config):
}, blocking=True)
else:
logger.error(
'Got unsupported command %s from text %s', command, text)
logger.error('Got unsupported command %s from text %s',
command, text)
hass.services.register(DOMAIN, SERVICE_PROCESS, process,
schema=SERVICE_PROCESS_SCHEMA)

View File

@ -377,12 +377,16 @@ def load_config(path, hass, consider_home, home_range):
"""Load devices from YAML configuration file."""
if not os.path.isfile(path):
return []
try:
return [
Device(hass, consider_home, home_range, device.get('track', False),
str(dev_id).lower(), str(device.get('mac')).upper(),
device.get('name'), device.get('picture'),
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
for dev_id, device in load_yaml_config_file(path).items()]
except HomeAssistantError:
# When YAML file could not be loaded/did not contain a dict
return []
def setup_scanner_platform(hass, config, scanner, see_device):

View File

@ -62,8 +62,9 @@ def get_scanner(hass, config):
_LOGGER):
return None
elif CONF_PASSWORD not in config[DOMAIN] and \
'ssh_key' not in config[DOMAIN] and \
'pub_key' not in config[DOMAIN]:
_LOGGER.error("Either a public key or password must be provided")
_LOGGER.error('Either a private key or password must be provided')
return None
scanner = AsusWrtDeviceScanner(config[DOMAIN])
@ -83,8 +84,8 @@ class AsusWrtDeviceScanner(object):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = str(config[CONF_USERNAME])
self.password = str(config.get(CONF_PASSWORD, ""))
self.pub_key = str(config.get('pub_key', ""))
self.password = str(config.get(CONF_PASSWORD, ''))
self.ssh_key = str(config.get('ssh_key', config.get('pub_key', '')))
self.protocol = config.get('protocol')
self.mode = config.get('mode')
@ -120,7 +121,7 @@ class AsusWrtDeviceScanner(object):
return False
with self.lock:
_LOGGER.info("Checking ARP")
_LOGGER.info('Checking ARP')
data = self.get_asuswrt_data()
if not data:
return False
@ -138,12 +139,12 @@ class AsusWrtDeviceScanner(object):
try:
ssh = pxssh.pxssh()
if self.pub_key:
ssh.login(self.host, self.username, ssh_key=self.pub_key)
if self.ssh_key:
ssh.login(self.host, self.username, ssh_key=self.ssh_key)
elif self.password:
ssh.login(self.host, self.username, self.password)
else:
_LOGGER.error('No password or public key specified')
_LOGGER.error('No password or private key specified')
return None
ssh.sendline(_IP_NEIGH_CMD)
ssh.prompt()
@ -195,16 +196,16 @@ class AsusWrtDeviceScanner(object):
telnet.write('exit\n'.encode('ascii'))
return AsusWrtResult(neighbors, leases_result, arp_result)
except EOFError:
_LOGGER.error("Unexpected response from router")
_LOGGER.error('Unexpected response from router')
return None
except ConnectionRefusedError:
_LOGGER.error("Connection refused by router, is telnet enabled?")
_LOGGER.error('Connection refused by router, is telnet enabled?')
return None
except socket.gaierror as exc:
_LOGGER.error("Socket exception: %s", exc)
_LOGGER.error('Socket exception: %s', exc)
return None
except OSError as exc:
_LOGGER.error("OSError: %s", exc)
_LOGGER.error('OSError: %s', exc)
return None
def get_asuswrt_data(self):
@ -232,7 +233,7 @@ class AsusWrtDeviceScanner(object):
match = _WL_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse wl row: %s", lease)
_LOGGER.warning('Could not parse wl row: %s', lease)
continue
host = ''
@ -242,7 +243,7 @@ class AsusWrtDeviceScanner(object):
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)
_LOGGER.warning('Could not parse arp row: %s', arp)
continue
devices[arp_match.group('ip')] = {
@ -256,7 +257,7 @@ class AsusWrtDeviceScanner(object):
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse lease row: %s", lease)
_LOGGER.warning('Could not parse lease row: %s', lease)
continue
# For leases where the client doesn't set a hostname, ensure it
@ -275,7 +276,7 @@ class AsusWrtDeviceScanner(object):
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)
_LOGGER.warning('Could not parse neighbor row: %s', neighbor)
continue
if match.group('ip') in devices:
devices[match.group('ip')]['status'] = match.group('status')

View File

@ -16,6 +16,7 @@ REQUIREMENTS = ['urllib3', 'unifi==1.2.5']
_LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port'
CONF_SITE_ID = 'site_id'
def get_scanner(hass, config):
@ -32,6 +33,7 @@ def get_scanner(hass, config):
host = this_config.get(CONF_HOST, 'localhost')
username = this_config.get(CONF_USERNAME)
password = this_config.get(CONF_PASSWORD)
site_id = this_config.get(CONF_SITE_ID, 'default')
try:
port = int(this_config.get(CONF_PORT, 8443))
@ -40,7 +42,7 @@ def get_scanner(hass, config):
return False
try:
ctrl = Controller(host, username, password, port, 'v4')
ctrl = Controller(host, username, password, port, 'v4', site_id)
except urllib.error.HTTPError as ex:
_LOGGER.error('Failed to connect to unifi: %s', ex)
return False

View File

@ -149,7 +149,7 @@ def setup(hass, base_config):
EVL_CONTROLLER.stop()
def start_envisalink(event):
"""Startup process for the envisalink."""
"""Startup process for the Envisalink."""
EVL_CONTROLLER.start()
for _ in range(10):
if 'success' in _connect_status:
@ -175,7 +175,7 @@ def setup(hass, base_config):
if not _result:
return False
# Load sub-components for envisalink
# Load sub-components for Envisalink
if _partitions:
load_platform(hass, 'alarm_control_panel', 'envisalink',
{'partitions': _partitions,
@ -191,7 +191,7 @@ def setup(hass, base_config):
class EnvisalinkDevice(Entity):
"""Representation of an envisalink devicetity."""
"""Representation of an Envisalink device."""
def __init__(self, name, info, controller):
"""Initialize the device."""

View File

@ -1,9 +1,9 @@
"""Handle the frontend for Home Assistant."""
import os
from . import version, mdi_version
from homeassistant.components import api
from homeassistant.components.http import HomeAssistantView
from . import version, mdi_version
DOMAIN = 'frontend'
DEPENDENCIES = ['api']
@ -76,11 +76,17 @@ class IndexView(HomeAssistantView):
def get(self, request, entity_id=None):
"""Serve the index view."""
if self.hass.wsgi.development:
core_url = 'home-assistant-polymer/build/_core_compiled.js'
ui_url = 'home-assistant-polymer/src/home-assistant.html'
core_url = '/static/home-assistant-polymer/build/_core_compiled.js'
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
map_url = ('/static/home-assistant-polymer/src/layouts/'
'partial-map.html')
dev_url = ('/static/home-assistant-polymer/src/entry-points/'
'dev-tools.html')
else:
core_url = 'core-{}.js'.format(version.CORE)
ui_url = 'frontend-{}.html'.format(version.UI)
core_url = '/static/core-{}.js'.format(version.CORE)
ui_url = '/static/frontend-{}.html'.format(version.UI)
map_url = '/static/partial-map-{}.html'.format(version.MAP)
dev_url = '/static/dev-tools-{}.html'.format(version.DEV)
# auto login if no password was set
if self.hass.config.api.api_password is None:
@ -88,14 +94,14 @@ class IndexView(HomeAssistantView):
else:
auth = 'false'
icons_url = 'mdi-{}.html'.format(mdi_version.VERSION)
icons_url = '/static/mdi-{}.html'.format(mdi_version.VERSION)
template = self.templates.get_template('index.html')
# pylint is wrong
# pylint: disable=no-member
resp = template.render(
core_url=core_url, ui_url=ui_url, auth=auth,
icons_url=icons_url, icons=mdi_version.VERSION)
core_url=core_url, ui_url=ui_url, map_url=map_url, auth=auth,
dev_url=dev_url, icons_url=icons_url, icons=mdi_version.VERSION)
return self.Response(resp, mimetype='text/html')

View File

@ -1,2 +1,2 @@
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
VERSION = "9ee3d4466a65bef35c2c8974e91b37c0"
VERSION = "758957b7ea989d6beca60e218ea7f7dd"

View File

@ -64,8 +64,12 @@
document
.getElementById('ha-init-skeleton')
.classList.add('error');
}
window.noAuth = {{ auth }}
};
window.noAuth = {{ auth }};
window.deferredLoading = {
map: '{{ map_url }}',
dev: '{{ dev_url }}',
};
</script>
</head>
<body fullbleed>
@ -76,9 +80,9 @@
</div>
<home-assistant icons='{{ icons }}'></home-assistant>
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
<script src='/static/{{ core_url }}'></script>
<link rel='import' href='/static/{{ ui_url }}' onerror='initError()' async>
<link rel='import' href='/static/{{ icons_url }}' async>
<script src='{{ core_url }}'></script>
<link rel='import' href='{{ ui_url }}' onerror='initError()' async>
<link rel='import' href='{{ icons_url }}' async>
<script>
var webComponentsSupported = (
'registerElement' in document &&

View File

@ -1,3 +1,5 @@
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
CORE = "db0bb387f4d3bcace002d62b94baa348"
UI = "5b306b7e7d36799b7b67f592cbe94703"
CORE = "7d80cc0e4dea6bc20fa2889be0b3cd15"
UI = "805f8dda70419b26daabc8e8f625127f"
MAP = "c922306de24140afd14f857f927bf8f0"
DEV = "b7079ac3121b95b9856e5603a6d8a263"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 1e1a3a1c845713508d21d7c1cb87a7ecee6222aa
Subproject commit 5e7f2fdbe849c43ba1c7dd647e5f948894c3118e

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -29,7 +29,7 @@
/* eslint-disable quotes, comma-spacing */
var PrecacheConfig = [["/","70eeeca780a5f23c7632c2876dd1795a"],["/devEvent","70eeeca780a5f23c7632c2876dd1795a"],["/devInfo","70eeeca780a5f23c7632c2876dd1795a"],["/devService","70eeeca780a5f23c7632c2876dd1795a"],["/devState","70eeeca780a5f23c7632c2876dd1795a"],["/devTemplate","70eeeca780a5f23c7632c2876dd1795a"],["/history","70eeeca780a5f23c7632c2876dd1795a"],["/logbook","70eeeca780a5f23c7632c2876dd1795a"],["/map","70eeeca780a5f23c7632c2876dd1795a"],["/states","70eeeca780a5f23c7632c2876dd1795a"],["/static/core-db0bb387f4d3bcace002d62b94baa348.js","f938163a392465dc87af3a0094376621"],["/static/frontend-5b306b7e7d36799b7b67f592cbe94703.html","70eeeca780a5f23c7632c2876dd1795a"],["/static/mdi-9ee3d4466a65bef35c2c8974e91b37c0.html","9a6846935116cd29279c91e0ee0a26d0"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
var PrecacheConfig = [["/","d2c67846acf9a583c29798c30503cbf1"],["/devEvent","c4cdd84093404ee3fe0896070ebde97f"],["/devInfo","c4cdd84093404ee3fe0896070ebde97f"],["/devService","c4cdd84093404ee3fe0896070ebde97f"],["/devState","c4cdd84093404ee3fe0896070ebde97f"],["/devTemplate","c4cdd84093404ee3fe0896070ebde97f"],["/history","d2c67846acf9a583c29798c30503cbf1"],["/logbook","d2c67846acf9a583c29798c30503cbf1"],["/map","df0c87260b6dd990477cda43a2440b1c"],["/states","d2c67846acf9a583c29798c30503cbf1"],["/static/core-7d80cc0e4dea6bc20fa2889be0b3cd15.js","1f35577e9f32a86a03944e5e8d15eab2"],["/static/dev-tools-b7079ac3121b95b9856e5603a6d8a263.html","4ba7c57b48c9d28a1e0d9d7624b83700"],["/static/frontend-805f8dda70419b26daabc8e8f625127f.html","d8eeb403baf5893de8404beec0135d96"],["/static/mdi-758957b7ea989d6beca60e218ea7f7dd.html","4c32b01a3a5b194630963ff7ec4df36f"],["/static/partial-map-c922306de24140afd14f857f927bf8f0.html","853772ea26ac2f4db0f123e20c1ca160"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
/* eslint-enable quotes, comma-spacing */
var CacheNamePrefix = 'sw-precache-v1--' + (self.registration ? self.registration.scope : '') + '-';

View File

@ -57,7 +57,7 @@ class RPiGPIOGarageDoor(GarageDoorDevice):
self._relay_pin = relay_pin
self._state_pin = state_pin
rpi_gpio.setup_output(self._relay_pin)
rpi_gpio.setup_input(self._state_pin, 'DOWN')
rpi_gpio.setup_input(self._state_pin, 'UP')
rpi_gpio.write_output(self._relay_pin, True)
@property

View File

@ -10,7 +10,7 @@ from homeassistant.components.garage_door import GarageDoorDevice
from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -9,8 +9,8 @@ from collections import defaultdict
from datetime import timedelta
from itertools import groupby
from homeassistant.components import recorder, script
import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, script
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'history'
@ -27,13 +27,12 @@ def last_5_states(entity_id):
"""Return the last 5 states for entity_id."""
entity_id = entity_id.lower()
query = """
SELECT * FROM states WHERE entity_id=? AND
last_changed=last_updated
ORDER BY state_id DESC LIMIT 0, 5
"""
return recorder.query_states(query, (entity_id, ))
states = recorder.get_model('States')
return recorder.execute(
recorder.query('States').filter(
(states.entity_id == entity_id) &
(states.last_changed == states.last_updated)
).order_by(states.state_id.desc()).limit(5))
def get_significant_states(start_time, end_time=None, entity_id=None):
@ -44,26 +43,22 @@ def get_significant_states(start_time, end_time=None, entity_id=None):
as well as all states from certain domains (for instance
thermostat so that we get current temperature in our graphs).
"""
where = """
(domain IN ({}) OR last_changed=last_updated)
AND domain NOT IN ({}) AND last_updated > ?
""".format(",".join("'%s'" % x for x in SIGNIFICANT_DOMAINS),
",".join("'%s'" % x for x in IGNORE_DOMAINS))
data = [start_time]
states = recorder.get_model('States')
query = recorder.query('States').filter(
(states.domain.in_(SIGNIFICANT_DOMAINS) |
(states.last_changed == states.last_updated)) &
((~states.domain.in_(IGNORE_DOMAINS)) &
(states.last_updated > start_time)))
if end_time is not None:
where += "AND last_updated < ? "
data.append(end_time)
query = query.filter(states.last_updated < end_time)
if entity_id is not None:
where += "AND entity_id = ? "
data.append(entity_id.lower())
query = query.filter_by(entity_id=entity_id.lower())
query = ("SELECT * FROM states WHERE {} "
"ORDER BY entity_id, last_updated ASC").format(where)
states = (state for state in recorder.query_states(query, data)
states = (
state for state in recorder.execute(
query.order_by(states.entity_id, states.last_updated))
if _is_significant(state))
return states_to_json(states, start_time, entity_id)
@ -71,21 +66,19 @@ def get_significant_states(start_time, end_time=None, entity_id=None):
def state_changes_during_period(start_time, end_time=None, entity_id=None):
"""Return states changes during UTC period start_time - end_time."""
where = "last_changed=last_updated AND last_changed > ? "
data = [start_time]
states = recorder.get_model('States')
query = recorder.query('States').filter(
(states.last_changed == states.last_updated) &
(states.last_changed > start_time))
if end_time is not None:
where += "AND last_changed < ? "
data.append(end_time)
query = query.filter(states.last_updated < end_time)
if entity_id is not None:
where += "AND entity_id = ? "
data.append(entity_id.lower())
query = query.filter_by(entity_id=entity_id.lower())
query = ("SELECT * FROM states WHERE {} "
"ORDER BY entity_id, last_changed ASC").format(where)
states = recorder.query_states(query, data)
states = recorder.execute(
query.order_by(states.entity_id, states.last_updated))
return states_to_json(states, start_time, entity_id)
@ -99,24 +92,27 @@ def get_states(utc_point_in_time, entity_ids=None, run=None):
if run is None:
return []
where = run.where_after_start_run + "AND created < ? "
where_data = [utc_point_in_time]
from sqlalchemy import and_, func
states = recorder.get_model('States')
most_recent_state_ids = recorder.query(
func.max(states.state_id).label('max_state_id')
).filter(
(states.created >= run.start) &
(states.created < utc_point_in_time)
)
if entity_ids is not None:
where += "AND entity_id IN ({}) ".format(
",".join(['?'] * len(entity_ids)))
where_data.extend(entity_ids)
most_recent_state_ids = most_recent_state_ids.filter(
states.entity_id.in_(entity_ids))
query = """
SELECT * FROM states
INNER JOIN (
SELECT max(state_id) AS max_state_id
FROM states WHERE {}
GROUP BY entity_id)
WHERE state_id = max_state_id
""".format(where)
most_recent_state_ids = most_recent_state_ids.group_by(
states.entity_id).subquery()
return recorder.query_states(query, where_data)
query = recorder.query('States').join(most_recent_state_ids, and_(
states.state_id == most_recent_state_ids.c.max_state_id))
return recorder.execute(query)
def states_to_json(states, start_time, entity_id):

View File

@ -4,71 +4,120 @@ Support for Homematic devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematic/
"""
import os
import time
import logging
from functools import partial
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN
from homeassistant.helpers import discovery
import voluptuous as vol
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN,
CONF_USERNAME, CONF_PASSWORD)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers import discovery
from homeassistant.config import load_yaml_config_file
DOMAIN = 'homematic'
REQUIREMENTS = ['pyhomematic==0.1.8']
REQUIREMENTS = ["pyhomematic==0.1.9"]
HOMEMATIC = None
HOMEMATIC_LINK_DELAY = 0.5
DISCOVER_SWITCHES = "homematic.switch"
DISCOVER_LIGHTS = "homematic.light"
DISCOVER_SENSORS = "homematic.sensor"
DISCOVER_BINARY_SENSORS = "homematic.binary_sensor"
DISCOVER_ROLLERSHUTTER = "homematic.rollershutter"
DISCOVER_THERMOSTATS = "homematic.thermostat"
DISCOVER_SWITCHES = 'homematic.switch'
DISCOVER_LIGHTS = 'homematic.light'
DISCOVER_SENSORS = 'homematic.sensor'
DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor'
DISCOVER_ROLLERSHUTTER = 'homematic.rollershutter'
DISCOVER_THERMOSTATS = 'homematic.thermostat'
ATTR_DISCOVER_DEVICES = "devices"
ATTR_PARAM = "param"
ATTR_CHANNEL = "channel"
ATTR_NAME = "name"
ATTR_ADDRESS = "address"
ATTR_DISCOVER_DEVICES = 'devices'
ATTR_PARAM = 'param'
ATTR_CHANNEL = 'channel'
ATTR_NAME = 'name'
ATTR_ADDRESS = 'address'
EVENT_KEYPRESS = "homematic.keypress"
EVENT_KEYPRESS = 'homematic.keypress'
EVENT_IMPULSE = 'homematic.impulse'
SERVICE_VIRTUALKEY = 'virtualkey'
HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: ["Switch", "SwitchPowermeter"],
DISCOVER_LIGHTS: ["Dimmer"],
DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2",
"RemoteMotion", "ThermostatWall", "AreaThermostat",
"RotaryHandleSensor", "WaterSensor"],
DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"],
DISCOVER_BINARY_SENSORS: ["ShutterContact", "Smoke", "SmokeV2",
"Motion", "MotionV2", "RemoteMotion"],
DISCOVER_ROLLERSHUTTER: ["Blind"]
DISCOVER_SWITCHES: ['Switch', 'SwitchPowermeter'],
DISCOVER_LIGHTS: ['Dimmer'],
DISCOVER_SENSORS: ['SwitchPowermeter', 'Motion', 'MotionV2',
'RemoteMotion', 'ThermostatWall', 'AreaThermostat',
'RotaryHandleSensor', 'WaterSensor', 'PowermeterGas',
'LuxSensor'],
DISCOVER_THERMOSTATS: ['Thermostat', 'ThermostatWall', 'MAXThermostat'],
DISCOVER_BINARY_SENSORS: ['ShutterContact', 'Smoke', 'SmokeV2', 'Motion',
'MotionV2', 'RemoteMotion'],
DISCOVER_ROLLERSHUTTER: ['Blind']
}
HM_IGNORE_DISCOVERY_NODE = [
"ACTUAL_TEMPERATURE"
'ACTUAL_TEMPERATURE'
]
HM_ATTRIBUTE_SUPPORT = {
"LOWBAT": ["Battery", {0: "High", 1: "Low"}],
"ERROR": ["Sabotage", {0: "No", 1: "Yes"}],
"RSSI_DEVICE": ["RSSI", {}],
"VALVE_STATE": ["Valve", {}],
"BATTERY_STATE": ["Battery", {}],
"CONTROL_MODE": ["Mode", {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost"}],
"POWER": ["Power", {}],
"CURRENT": ["Current", {}],
"VOLTAGE": ["Voltage", {}]
'LOWBAT': ['Battery', {0: 'High', 1: 'Low'}],
'ERROR': ['Sabotage', {0: 'No', 1: 'Yes'}],
'RSSI_DEVICE': ['RSSI', {}],
'VALVE_STATE': ['Valve', {}],
'BATTERY_STATE': ['Battery', {}],
'CONTROL_MODE': ['Mode', {0: 'Auto', 1: 'Manual', 2: 'Away', 3: 'Boost'}],
'POWER': ['Power', {}],
'CURRENT': ['Current', {}],
'VOLTAGE': ['Voltage', {}]
}
HM_PRESS_EVENTS = [
"PRESS_SHORT",
"PRESS_LONG",
"PRESS_CONT",
"PRESS_LONG_RELEASE"
'PRESS_SHORT',
'PRESS_LONG',
'PRESS_CONT',
'PRESS_LONG_RELEASE'
]
HM_IMPULSE_EVENTS = [
'SEQUENCE_OK'
]
_LOGGER = logging.getLogger(__name__)
CONF_RESOLVENAMES_OPTIONS = [
'metadata',
'json',
'xml',
False
]
CONF_LOCAL_IP = 'local_ip'
CONF_LOCAL_PORT = 'local_port'
CONF_REMOTE_IP = 'remote_ip'
CONF_REMOTE_PORT = 'remote_port'
CONF_RESOLVENAMES = 'resolvenames'
CONF_DELAY = 'delay'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_LOCAL_IP): vol.Coerce(str),
vol.Optional(CONF_LOCAL_PORT, default=8943):
vol.All(vol.Coerce(int),
vol.Range(min=1, max=65535)),
vol.Required(CONF_REMOTE_IP): vol.Coerce(str),
vol.Optional(CONF_REMOTE_PORT, default=2001):
vol.All(vol.Coerce(int),
vol.Range(min=1, max=65535)),
vol.Optional(CONF_RESOLVENAMES, default=False):
vol.In(CONF_RESOLVENAMES_OPTIONS),
vol.Optional(CONF_USERNAME, default="Admin"): vol.Coerce(str),
vol.Optional(CONF_PASSWORD, default=""): vol.Coerce(str),
vol.Optional(CONF_DELAY, default=0.5): vol.Coerce(float)
})
SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({
vol.Required(ATTR_ADDRESS): vol.Coerce(str),
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
vol.Required(ATTR_PARAM): vol.Coerce(str)
})
# pylint: disable=unused-argument
def setup(hass, config):
@ -77,14 +126,14 @@ def setup(hass, config):
from pyhomematic import HMConnection
local_ip = config[DOMAIN].get("local_ip", None)
local_port = config[DOMAIN].get("local_port", 8943)
remote_ip = config[DOMAIN].get("remote_ip", None)
remote_port = config[DOMAIN].get("remote_port", 2001)
resolvenames = config[DOMAIN].get("resolvenames", False)
username = config[DOMAIN].get("username", "Admin")
password = config[DOMAIN].get("password", "")
HOMEMATIC_LINK_DELAY = config[DOMAIN].get("delay", 0.5)
local_ip = config[DOMAIN][0].get(CONF_LOCAL_IP)
local_port = config[DOMAIN][0].get(CONF_LOCAL_PORT)
remote_ip = config[DOMAIN][0].get(CONF_REMOTE_IP)
remote_port = config[DOMAIN][0].get(CONF_REMOTE_PORT)
resolvenames = config[DOMAIN][0].get(CONF_RESOLVENAMES)
username = config[DOMAIN][0].get(CONF_USERNAME)
password = config[DOMAIN][0].get(CONF_PASSWORD)
HOMEMATIC_LINK_DELAY = config[DOMAIN][0].get(CONF_DELAY)
if remote_ip is None or local_ip is None:
_LOGGER.error("Missing remote CCU/Homegear or local address")
@ -109,6 +158,15 @@ def setup(hass, config):
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, HOMEMATIC.stop)
hass.config.components.append(DOMAIN)
# regeister homematic services
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_VIRTUALKEY,
_hm_service_virtualkey,
descriptions[DOMAIN][SERVICE_VIRTUALKEY],
SCHEMA_SERVICE_VIRTUALKEY)
return True
@ -302,7 +360,7 @@ def _hm_event_handler(hass, device, caller, attribute, value):
_LOGGER.debug("Event %s for %s channel %i", attribute,
hmdevice.NAME, channel)
# a keypress event
# keypress event
if attribute in HM_PRESS_EVENTS:
hass.bus.fire(EVENT_KEYPRESS, {
ATTR_NAME: hmdevice.NAME,
@ -311,9 +369,42 @@ def _hm_event_handler(hass, device, caller, attribute, value):
})
return
# impulse event
if attribute in HM_IMPULSE_EVENTS:
hass.bus.fire(EVENT_KEYPRESS, {
ATTR_NAME: hmdevice.NAME,
ATTR_CHANNEL: channel
})
return
_LOGGER.warning("Event is unknown and not forwarded to HA")
def _hm_service_virtualkey(call):
"""Callback for handle virtualkey services."""
address = call.data.get(ATTR_ADDRESS)
channel = call.data.get(ATTR_CHANNEL)
param = call.data.get(ATTR_PARAM)
if address not in HOMEMATIC.devices:
_LOGGER.error("%s not found for service virtualkey!", address)
return
hmdevice = HOMEMATIC.devices.get(address)
# if param exists for this device
if param not in hmdevice.ACTIONNODE:
_LOGGER.error("%s not datapoint in hm device %s", param, address)
return
# channel exists?
if channel > hmdevice.ELEMENT:
_LOGGER.error("%i is not a channel in hm device %s", channel, address)
return
# call key
hmdevice.actionNodeData(param, 1, channel)
class HMDevice(Entity):
"""The Homematic device base object."""
@ -465,7 +556,7 @@ class HMDevice(Entity):
channel = self._channel
# Prepare for subscription
try:
if int(channel) > 0:
if int(channel) >= 0:
channels_to_sub.update({int(channel): True})
except (ValueError, TypeError):
_LOGGER("Invalid channel in metadata from %s",

View File

@ -0,0 +1,80 @@
"""
Component for Joaoapps Join services.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/join/
"""
import logging
import voluptuous as vol
from homeassistant.const import CONF_NAME, CONF_API_KEY
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = [
'https://github.com/nkgilley/python-join-api/archive/'
'3e1e849f1af0b4080f551b62270c6d244d5fbcbd.zip#python-join-api==0.0.1']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'joaoapps_join'
CONF_DEVICE_ID = 'device_id'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE_ID): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_API_KEY): cv.string
})
}, extra=vol.ALLOW_EXTRA)
# pylint: disable=too-many-locals
def setup(hass, config):
"""Setup Join services."""
from pyjoin import (get_devices, ring_device, set_wallpaper, send_sms,
send_file, send_url, send_notification)
device_id = config[DOMAIN].get(CONF_DEVICE_ID)
api_key = config[DOMAIN].get(CONF_API_KEY)
name = config[DOMAIN].get(CONF_NAME)
if api_key:
if not get_devices(api_key):
_LOGGER.error("Error connecting to Join, check API key")
return False
def ring_service(service):
"""Service to ring devices."""
ring_device(device_id, api_key=api_key)
def set_wallpaper_service(service):
"""Service to set wallpaper on devices."""
set_wallpaper(device_id, url=service.data.get('url'), api_key=api_key)
def send_file_service(service):
"""Service to send files to devices."""
send_file(device_id, url=service.data.get('url'), api_key=api_key)
def send_url_service(service):
"""Service to open url on devices."""
send_url(device_id, url=service.data.get('url'), api_key=api_key)
def send_tasker_service(service):
"""Service to open url on devices."""
send_notification(device_id=device_id,
text=service.data.get('command'),
api_key=api_key)
def send_sms_service(service):
"""Service to send sms from devices."""
send_sms(device_id=device_id,
sms_number=service.data.get('number'),
sms_text=service.data.get('message'),
api_key=api_key)
name = name.lower().replace(" ", "_") + "_" if name else ""
hass.services.register(DOMAIN, name + 'ring', ring_service)
hass.services.register(DOMAIN, name + 'set_wallpaper',
set_wallpaper_service)
hass.services.register(DOMAIN, name + 'send_sms', send_sms_service)
hass.services.register(DOMAIN, name + 'send_file', send_file_service)
hass.services.register(DOMAIN, name + 'send_url', send_url_service)
hass.services.register(DOMAIN, name + 'send_tasker', send_tasker_service)
return True

View File

@ -0,0 +1,295 @@
"""
Support for KNX components.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/knx/
"""
import logging
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity
DOMAIN = "knx"
REQUIREMENTS = ['knxip==0.3.0']
EVENT_KNX_FRAME_RECEIVED = "knx_frame_received"
CONF_HOST = "host"
CONF_PORT = "port"
DEFAULT_PORT = "3671"
KNXTUNNEL = None
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
"""Setup the connection to the KNX IP interface."""
global KNXTUNNEL
from knxip.ip import KNXIPTunnel
from knxip.core import KNXException
host = config[DOMAIN].get(CONF_HOST, None)
if host is None:
_LOGGER.debug("Will try to auto-detect KNX/IP gateway")
host = "0.0.0.0"
try:
port = int(config[DOMAIN].get(CONF_PORT, DEFAULT_PORT))
except ValueError:
_LOGGER.exception("Can't parse KNX IP interface port")
return False
KNXTUNNEL = KNXIPTunnel(host, port)
try:
KNXTUNNEL.connect()
except KNXException as ex:
_LOGGER.exception("Can't connect to KNX/IP interface: %s", ex)
KNXTUNNEL = None
return False
_LOGGER.info("KNX IP tunnel to %s:%i established", host, port)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_tunnel)
return True
def close_tunnel(_data):
"""Close the NKX tunnel connection on shutdown."""
global KNXTUNNEL
KNXTUNNEL.disconnect()
KNXTUNNEL = None
class KNXConfig(object):
"""Handle the fetching of configuration from the config file."""
def __init__(self, config):
"""Initialize the configuration."""
from knxip.core import parse_group_address
self.config = config
self.should_poll = config.get("poll", True)
self._address = parse_group_address(config.get("address"))
if self.config.get("state_address"):
self._state_address = parse_group_address(
self.config.get("state_address"))
else:
self._state_address = None
@property
def name(self):
"""The name given to the entity."""
return self.config["name"]
@property
def address(self):
"""The address of the device as an integer value.
3 types of addresses are supported:
integer - 0-65535
2 level - a/b
3 level - a/b/c
"""
return self._address
@property
def state_address(self):
"""The group address the device sends its current state to.
Some KNX devices can send the current state to a seperate
group address. This makes send e.g. when an actuator can
be switched but also have a timer functionality.
"""
return self._state_address
class KNXGroupAddress(Entity):
"""Representation of devices connected to a KNX group address."""
def __init__(self, hass, config):
"""Initialize the device."""
self._config = config
self._state = False
self._data = None
_LOGGER.debug("Initalizing KNX group address %s", self.address)
def handle_knx_message(addr, data):
"""Handle an incoming KNX frame.
Handle an incoming frame and update our status if it contains
information relating to this device.
"""
if (addr == self.state_address) or (addr == self.address):
self._state = data
self.update_ha_state()
KNXTUNNEL.register_listener(self.address, handle_knx_message)
if self.state_address:
KNXTUNNEL.register_listener(self.state_address, handle_knx_message)
@property
def name(self):
"""The entity's display name."""
return self._config.name
@property
def config(self):
"""The entity's configuration."""
return self._config
@property
def should_poll(self):
"""Return the state of the polling, if needed."""
return self._config.should_poll
@property
def is_on(self):
"""Return True if the value is not 0 is on, else False."""
if self.should_poll:
self.update()
return self._state != 0
@property
def address(self):
"""Return the KNX group address."""
return self._config.address
@property
def state_address(self):
"""Return the KNX group address."""
return self._config.state_address
@property
def cache(self):
"""The name given to the entity."""
return self._config.config.get("cache", True)
def group_write(self, value):
"""Write to the group address."""
KNXTUNNEL.group_write(self.address, [value])
def update(self):
"""Get the state from KNX bus or cache."""
from knxip.core import KNXException
try:
if self.state_address:
res = KNXTUNNEL.group_read(self.state_address,
use_cache=self.cache)
else:
res = KNXTUNNEL.group_read(self.address,
use_cache=self.cache)
if res:
self._state = res[0]
self._data = res
else:
_LOGGER.debug("Unable to read from KNX address: %s (None)",
self.address)
except KNXException:
_LOGGER.exception("Unable to read from KNX address: %s",
self.address)
return False
class KNXMultiAddressDevice(KNXGroupAddress):
"""Representation of devices connected to a multiple KNX group address.
This is needed for devices like dimmers or shutter actuators as they have
to be controlled by multiple group addresses.
"""
names = {}
values = {}
def __init__(self, hass, config, required, optional=None):
"""Initialize the device.
The namelist argument lists the required addresses. E.g. for a dimming
actuators, the namelist might look like:
onoff_address: 0/0/1
brightness_address: 0/0/2
"""
from knxip.core import parse_group_address, KNXException
super().__init__(self, hass, config)
self.config = config
# parse required addresses
for name in required:
paramname = name + "_address"
addr = self._config.config.get(paramname)
if addr is None:
_LOGGER.exception("Required KNX group address %s missing",
paramname)
raise KNXException("Group address missing in configuration")
addr = parse_group_address(addr)
self.names[addr] = name
# parse optional addresses
for name in optional:
paramname = name + "_address"
addr = self._config.config.get(paramname)
if addr:
try:
addr = parse_group_address(addr)
except KNXException:
_LOGGER.exception("Cannot parse group address %s", addr)
self.names[addr] = name
def handle_frame(frame):
"""Handle an incoming KNX frame.
Handle an incoming frame and update our status if it contains
information relating to this device.
"""
addr = frame.data[0]
if addr in self.names:
self.values[addr] = frame.data[1]
self.update_ha_state()
hass.bus.listen(EVENT_KNX_FRAME_RECEIVED, handle_frame)
def group_write_address(self, name, value):
"""Write to the group address with the given name."""
KNXTUNNEL.group_write(self.address, [value])
def has_attribute(self, name):
"""Check if the attribute with the given name is defined.
This is mostly important for optional addresses.
"""
for attributename, dummy_attribute in self.names.items():
if attributename == name:
return True
return False
def value(self, name):
"""Return the value to a given named attribute."""
from knxip.core import KNXException
addr = None
for attributename, attributeaddress in self.names.items():
if attributename == name:
addr = attributeaddress
if addr is None:
_LOGGER.exception("Attribute %s undefined", name)
return False
try:
res = KNXTUNNEL.group_read(addr, use_cache=self.cache)
except KNXException:
_LOGGER.exception("Unable to read from KNX address: %s",
addr)
return False
return res

View File

@ -67,12 +67,13 @@ PROP_TO_ATTR = {
# Service call validation schemas
VALID_TRANSITION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900))
VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
LIGHT_TURN_ON_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids,
ATTR_PROFILE: str,
ATTR_TRANSITION: VALID_TRANSITION,
ATTR_BRIGHTNESS: cv.byte,
ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_COLOR_NAME: str,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)),

View File

@ -18,7 +18,7 @@ LIGHT_TEMPS = [240, 380]
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup demo light platform."""
"""Setup the demo light platform."""
add_devices_callback([
DemoLight("Bed Light", False),
DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]),
@ -27,7 +27,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class DemoLight(Light):
"""Provide a demo light."""
"""Represenation of a demo light."""
# pylint: disable=too-many-arguments
def __init__(self, name, state, rgb=None, ct=None, brightness=180):

View File

@ -129,7 +129,7 @@ def setup_bridge(host, hass, add_devices_callback, filename,
new_lights = []
api_name = api.get('config').get('name')
if api_name == 'RaspBee-GW':
if api_name in ('RaspBee-GW', 'deCONZ-GW'):
bridge_type = 'deconz'
else:
bridge_type = 'hue'

View File

@ -14,7 +14,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.8', 'pubnub==3.7.6']
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):

View File

@ -14,16 +14,27 @@ from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
from homeassistant.components import zwave
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \
color_temperature_mired_to_kelvin, color_temperature_to_rgb
color_temperature_mired_to_kelvin, color_temperature_to_rgb, \
color_rgb_to_rgbw, color_rgbw_to_rgb
_LOGGER = logging.getLogger(__name__)
AEOTEC = 0x86
AEOTEC_ZW098_LED_BULB = 0x62
AEOTEC_ZW098_LED_BULB_LIGHT = (AEOTEC, AEOTEC_ZW098_LED_BULB)
COLOR_CHANNEL_WARM_WHITE = 0x01
COLOR_CHANNEL_COLD_WHITE = 0x02
COLOR_CHANNEL_RED = 0x04
COLOR_CHANNEL_GREEN = 0x08
COLOR_CHANNEL_BLUE = 0x10
WORKAROUND_ZW098 = 'zw098'
DEVICE_MAPPINGS = {
AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098
}
# Generate midpoint color temperatures for bulbs that have limited
# support for white light colors
TEMP_MID_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 2 + HASS_COLOR_MIN
@ -161,6 +172,7 @@ class ZwaveColorLight(ZwaveDimmer):
self._color_channels = None
self._rgb = None
self._ct = None
self._zw098 = None
# Here we attempt to find a zwave color value with the same instance
# id as the dimmer value. Currently zwave nodes that change colors
@ -182,6 +194,17 @@ class ZwaveColorLight(ZwaveDimmer):
if self._value_color_channels is None:
raise ValueError("Color Channels not found.")
# Make sure that we have values for the key before converting to int
if (value.node.manufacturer_id.strip() and
value.node.product_id.strip()):
specific_sensor_key = (int(value.node.manufacturer_id, 16),
int(value.node.product_id, 16))
if specific_sensor_key in DEVICE_MAPPINGS:
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098:
_LOGGER.debug("AEOTEC ZW098 workaround enabled")
self._zw098 = 1
super().__init__(value)
def update_properties(self):
@ -218,11 +241,10 @@ class ZwaveColorLight(ZwaveDimmer):
else:
cold_white = 0
# Color temperature. With two white channels, only two color
# temperatures are supported for the bulb. The channel values
# Color temperature. With the AEOTEC ZW098 bulb, only two color
# temperatures are supported. The warm and cold channel values
# indicate brightness for warm/cold color temperature.
if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and
self._color_channels & COLOR_CHANNEL_COLD_WHITE):
if self._zw098:
if warm_white > 0:
self._ct = TEMP_WARM_HASS
self._rgb = ct_to_rgb(self._ct)
@ -233,17 +255,11 @@ class ZwaveColorLight(ZwaveDimmer):
# RGB color is being used. Just report midpoint.
self._ct = TEMP_MID_HASS
# If only warm white is reported 0-255 is color temperature.
elif self._color_channels & COLOR_CHANNEL_WARM_WHITE:
self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * (
warm_white / 255)
self._rgb = ct_to_rgb(self._ct)
self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=warm_white))
# If only cold white is reported 0-255 is negative color temperature.
elif self._color_channels & COLOR_CHANNEL_COLD_WHITE:
self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * (
(255 - cold_white) / 255)
self._rgb = ct_to_rgb(self._ct)
self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=cold_white))
# If no rgb channels supported, report None.
if not (self._color_channels & COLOR_CHANNEL_RED or
@ -266,10 +282,10 @@ class ZwaveColorLight(ZwaveDimmer):
rgbw = None
if ATTR_COLOR_TEMP in kwargs:
# With two white channels, only two color temperatures are
# supported for the bulb.
if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and
self._color_channels & COLOR_CHANNEL_COLD_WHITE):
# Color temperature. With the AEOTEC ZW098 bulb, only two color
# temperatures are supported. The warm and cold channel values
# indicate brightness for warm/cold color temperature.
if self._zw098:
if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS:
self._ct = TEMP_WARM_HASS
rgbw = b'#000000FF00'
@ -277,25 +293,16 @@ class ZwaveColorLight(ZwaveDimmer):
self._ct = TEMP_COLD_HASS
rgbw = b'#00000000FF'
# If only warm white is reported 0-255 is color temperature
elif self._color_channels & COLOR_CHANNEL_WARM_WHITE:
rgbw = b'#000000'
temp = (
(kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) /
(HASS_COLOR_MAX - HASS_COLOR_MIN) * 255)
rgbw += format(int(temp)).encode('utf-8')
# If only cold white is reported 0-255 is negative color temp
elif self._color_channels & COLOR_CHANNEL_COLD_WHITE:
rgbw = b'#000000'
temp = (
255 - (kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) /
(HASS_COLOR_MAX - HASS_COLOR_MIN) * 255)
rgbw += format(int(temp)).encode('utf-8')
elif ATTR_RGB_COLOR in kwargs:
self._rgb = kwargs[ATTR_RGB_COLOR]
if (not self._zw098 and (
self._color_channels & COLOR_CHANNEL_WARM_WHITE or
self._color_channels & COLOR_CHANNEL_COLD_WHITE)):
rgbw = b'#'
for colorval in color_rgb_to_rgbw(*self._rgb):
rgbw += format(colorval, '02x').encode('utf-8')
rgbw += b'00'
else:
rgbw = b'#'
for colorval in self._rgb:
rgbw += format(colorval, '02x').encode('utf-8')

View File

@ -39,7 +39,7 @@ class VerisureDoorlock(LockDevice):
@property
def name(self):
"""Return the name of the lock."""
return 'Lock {}'.format(self._id)
return '{}'.format(hub.lock_status[self._id].location)
@property
def state(self):

View File

@ -10,7 +10,7 @@ from homeassistant.components.lock import LockDevice
from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -11,27 +11,23 @@ from itertools import groupby
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components import recorder, sun
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
STATE_NOT_HOME, STATE_OFF, STATE_ON)
from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.core import State
from homeassistant.helpers.entity import split_entity_id
from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView
from homeassistant.helpers.entity import split_entity_id
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
"""
_LOGGER = logging.getLogger(__name__)
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
@ -98,11 +94,14 @@ class LogbookView(HomeAssistantView):
else:
start_day = dt_util.start_of_local_day()
start_day = dt_util.as_utc(start_day)
end_day = start_day + timedelta(days=1)
events = recorder.query_events(
QUERY_EVENTS_BETWEEN,
(dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
events = recorder.get_model('Events')
query = recorder.query('Events').filter(
(events.time_fired > start_day) &
(events.time_fired < end_day))
events = recorder.execute(query)
return self.json(humanify(events))

View File

@ -31,6 +31,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source'
SERVICE_CLEAR_PLAYLIST = 'clear_playlist'
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
@ -75,6 +76,7 @@ SUPPORT_PLAY_MEDIA = 512
SUPPORT_VOLUME_STEP = 1024
SUPPORT_SELECT_SOURCE = 2048
SUPPORT_STOP = 4096
SUPPORT_CLEAR_PLAYLIST = 8192
# simple services that only take entity_id(s) as optional argument
SERVICE_TO_METHOD = {
@ -89,7 +91,8 @@ SERVICE_TO_METHOD = {
SERVICE_MEDIA_STOP: 'media_stop',
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
SERVICE_SELECT_SOURCE: 'select_source'
SERVICE_SELECT_SOURCE: 'select_source',
SERVICE_CLEAR_PLAYLIST: 'clear_playlist'
}
ATTR_TO_PROPERTY = [
@ -272,6 +275,12 @@ def select_source(hass, source, entity_id=None):
hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data)
def clear_playlist(hass, entity_id=None):
"""Send the media player the command for clear playlist."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data)
def setup(hass, config):
"""Track states and offer events for media_players."""
component = EntityComponent(
@ -542,6 +551,10 @@ class MediaPlayerDevice(Entity):
"""Select input source."""
raise NotImplementedError()
def clear_playlist(self):
"""Clear players playlist."""
raise NotImplementedError()
# No need to overwrite these.
@property
def support_pause(self):
@ -588,6 +601,11 @@ class MediaPlayerDevice(Entity):
"""Boolean if select source command supported."""
return bool(self.supported_media_commands & SUPPORT_SELECT_SOURCE)
@property
def support_clear_playlist(self):
"""Boolean if clear playlist command supported."""
return bool(self.supported_media_commands & SUPPORT_CLEAR_PLAYLIST)
def toggle(self):
"""Toggle the power on the media player."""
if self.state in [STATE_OFF, STATE_IDLE]:

View File

@ -17,8 +17,8 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON)
REQUIREMENTS = [
'https://github.com/aparraga/braviarc/archive/0.3.2.zip'
'#braviarc==0.3.2']
'https://github.com/aparraga/braviarc/archive/0.3.3.zip'
'#braviarc==0.3.3']
BRAVIA_CONFIG_FILE = 'bravia.conf'
CLIENTID_PREFIX = 'HomeAssistant'
@ -220,11 +220,13 @@ class BraviaTVDevice(MediaPlayerDevice):
self._refresh_volume()
self._refresh_channels()
power_status = self._braviarc.get_power_status()
if power_status == 'active':
self._state = STATE_ON
playing_info = self._braviarc.get_playing_info()
if playing_info is None or len(playing_info) == 0:
self._state = STATE_OFF
self._channel_name = 'App'
else:
self._state = STATE_ON
self._program_name = playing_info.get('programTitle')
self._channel_name = playing_info.get('title')
self._program_media_type = playing_info.get(
@ -234,6 +236,8 @@ class BraviaTVDevice(MediaPlayerDevice):
self._content_uri = playing_info.get('uri')
self._duration = playing_info.get('durationSec')
self._start_date_time = playing_info.get('startDateTime')
else:
self._state = STATE_OFF
except Exception as exception_instance: # pylint: disable=broad-except
_LOGGER.error(exception_instance)

View File

@ -8,7 +8,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,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
@ -32,7 +32,7 @@ YOUTUBE_PLAYER_SUPPORT = \
MUSIC_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST
NETFLIX_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
@ -214,12 +214,12 @@ class DemoMusicPlayer(AbstractDemoPlayer):
@property
def media_title(self):
"""Return the title of current playing media."""
return self.tracks[self._cur_track][1]
return self.tracks[self._cur_track][1] if len(self.tracks) > 0 else ""
@property
def media_artist(self):
"""Return the artist of current playing media (Music track only)."""
return self.tracks[self._cur_track][0]
return self.tracks[self._cur_track][0] if len(self.tracks) > 0 else ""
@property
def media_album_name(self):
@ -257,6 +257,13 @@ class DemoMusicPlayer(AbstractDemoPlayer):
self._cur_track += 1
self.update_ha_state()
def clear_playlist(self):
"""Clear players playlist."""
self.tracks = []
self._cur_track = 0
self._player_state = STATE_OFF
self.update_ha_state()
class DemoTVShowPlayer(AbstractDemoPlayer):
"""A Demo media player that only supports YouTube."""

View File

@ -122,7 +122,11 @@ def setup_plexserver(host, token, hass, add_devices_callback):
try:
devices = plexserver.clients()
except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error listing plex devices")
_LOGGER.exception('Error listing plex devices')
return
except OSError:
_LOGGER.error(
'Could not connect to plex server at http://%s', host)
return
new_plex_clients = []
@ -148,7 +152,7 @@ def setup_plexserver(host, token, hass, add_devices_callback):
try:
sessions = plexserver.sessions()
except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error listing plex sessions")
_LOGGER.exception('Error listing plex sessions')
return
plex_sessions.clear()
@ -166,7 +170,7 @@ def request_configuration(host, hass, add_devices_callback):
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING[host], "Failed to register, please try again.")
_CONFIGURING[host], 'Failed to register, please try again.')
return
@ -175,10 +179,10 @@ def request_configuration(host, hass, add_devices_callback):
setup_plexserver(host, data.get('token'), hass, add_devices_callback)
_CONFIGURING[host] = configurator.request_config(
hass, "Plex Media Server", plex_configuration_callback,
hass, 'Plex Media Server', plex_configuration_callback,
description=('Enter the X-Plex-Token'),
description_image="/static/images/config_plex_mediaserver.png",
submit_caption="Confirm",
description_image='/static/images/config_plex_mediaserver.png',
submit_caption='Confirm',
fields=[{'id': 'token', 'name': 'X-Plex-Token', 'type': ''}]
)
@ -201,7 +205,7 @@ class PlexClient(MediaPlayerDevice):
@property
def unique_id(self):
"""Return the id of this plex client."""
return "{}.{}".format(
return '{}.{}'.format(
self.__class__, self.device.machineIdentifier or self.device.name)
@property

View File

@ -15,8 +15,8 @@ from homeassistant.const import (
CONF_HOST, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, STATE_HOME)
REQUIREMENTS = [
'https://github.com/bah2830/python-roku/archive/3.1.1.zip'
'#python-roku==3.1.1']
'https://github.com/bah2830/python-roku/archive/3.1.2.zip'
'#roku==3.1.2']
KNOWN_HOSTS = []
DEFAULT_PORT = 8060
@ -45,6 +45,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
rokus = []
for host in hosts:
new_roku = RokuDevice(host)
if new_roku.name is None:
_LOGGER.error("Unable to initialize roku at %s", host)
else:
rokus.append(RokuDevice(host))
KNOWN_HOSTS.append(host)
@ -61,6 +66,11 @@ class RokuDevice(MediaPlayerDevice):
from roku import Roku
self.roku = Roku(host)
self.roku_name = None
self.ip_address = host
self.channels = []
self.current_app = None
self.update()
def update(self):
@ -78,7 +88,7 @@ class RokuDevice(MediaPlayerDevice):
self.current_app = None
except (requests.exceptions.ConnectionError,
requests.exceptions.ReadTimeout):
self.current_app = None
_LOGGER.error("Unable to connect to roku at %s", self.ip_address)
def get_source_list(self):
"""Get the list of applications to be used as sources."""

View File

@ -146,6 +146,14 @@ select_source:
description: Name of the source to switch to. Platform dependent.
example: 'video1'
clear_playlist:
description: Send the media player the command to clear players playlist.
fields:
entity_id:
description: Name(s) of entites to change source on
example: 'media_player.living_room_chromecast'
sonos_group_players:
description: Send Sonos media player the command for grouping all players into one (party mode).

View File

@ -12,7 +12,8 @@ from os import path
from homeassistant.components.media_player import (
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF)
from homeassistant.config import load_yaml_config_file
@ -31,13 +32,17 @@ _REQUESTS_LOGGER.setLevel(logging.ERROR)
SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\
SUPPORT_SEEK
SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST | SUPPORT_SELECT_SOURCE
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
SERVICE_UNJOIN = 'sonos_unjoin'
SERVICE_SNAPSHOT = 'sonos_snapshot'
SERVICE_RESTORE = 'sonos_restore'
SUPPORT_SOURCE_LINEIN = 'Line-in'
SUPPORT_SOURCE_TV = 'TV'
SUPPORT_SOURCE_RADIO = 'Radio'
# pylint: disable=unused-argument, too-many-locals
def setup_platform(hass, config, add_devices, discovery_info=None):
@ -162,12 +167,12 @@ class SonosDevice(MediaPlayerDevice):
# pylint: disable=too-many-arguments
def __init__(self, hass, player):
"""Initialize the Sonos device."""
from soco.snapshot import Snapshot
self.hass = hass
self.volume_increment = 5
super(SonosDevice, self).__init__()
self._player = player
self.update()
from soco.snapshot import Snapshot
self.soco_snapshot = Snapshot(self._player)
@property
@ -261,6 +266,10 @@ class SonosDevice(MediaPlayerDevice):
@property
def media_title(self):
"""Title of current playing media."""
if self._player.is_playing_line_in:
return SUPPORT_SOURCE_LINEIN
if self._player.is_playing_tv:
return SUPPORT_SOURCE_TV
if 'artist' in self._trackinfo and 'title' in self._trackinfo:
return '{artist} - {title}'.format(
artist=self._trackinfo['artist'],
@ -290,6 +299,36 @@ class SonosDevice(MediaPlayerDevice):
"""Mute (true) or unmute (false) media player."""
self._player.mute = mute
def select_source(self, source):
"""Select input source."""
if source == SUPPORT_SOURCE_LINEIN:
self._player.switch_to_line_in()
elif source == SUPPORT_SOURCE_TV:
self._player.switch_to_tv()
@property
def source_list(self):
"""List of available input sources."""
source = []
# generate list of supported device
source.append(SUPPORT_SOURCE_LINEIN)
source.append(SUPPORT_SOURCE_TV)
source.append(SUPPORT_SOURCE_RADIO)
return source
@property
def source(self):
"""Name of the current input source."""
if self._player.is_playing_line_in:
return SUPPORT_SOURCE_LINEIN
if self._player.is_playing_tv:
return SUPPORT_SOURCE_TV
if self._player.is_playing_radio:
return SUPPORT_SOURCE_RADIO
return None
@only_if_coordinator
def turn_off(self):
"""Turn off media player."""
@ -320,6 +359,11 @@ class SonosDevice(MediaPlayerDevice):
"""Send seek command."""
self._player.seek(str(datetime.timedelta(seconds=int(position))))
@only_if_coordinator
def clear_playlist(self):
"""Clear players playlist."""
self._player.clear_queue()
@only_if_coordinator
def turn_on(self):
"""Turn the media player on."""

View File

@ -17,8 +17,9 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED,
ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, ATTR_INPUT_SOURCE,
SERVICE_SELECT_SOURCE, MediaPlayerDevice)
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST,
ATTR_INPUT_SOURCE, SERVICE_SELECT_SOURCE, SERVICE_CLEAR_PLAYLIST,
MediaPlayerDevice)
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, CONF_NAME, SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE,
@ -346,9 +347,12 @@ class UniversalMediaPlayer(MediaPlayerDevice):
ATTR_MEDIA_VOLUME_MUTED in self._attrs:
flags |= SUPPORT_VOLUME_MUTE
if SUPPORT_SELECT_SOURCE in self._cmds:
if SERVICE_SELECT_SOURCE in self._cmds:
flags |= SUPPORT_SELECT_SOURCE
if SERVICE_CLEAR_PLAYLIST in self._cmds:
flags |= SUPPORT_CLEAR_PLAYLIST
return flags
@property
@ -424,6 +428,10 @@ class UniversalMediaPlayer(MediaPlayerDevice):
data = {ATTR_INPUT_SOURCE: source}
self._call_service(SERVICE_SELECT_SOURCE, data)
def clear_playlist(self):
"""Clear players playlist."""
self._call_service(SERVICE_CLEAR_PLAYLIST)
def update(self):
"""Update state in HA."""
for child_name in self._children:

View File

@ -38,7 +38,7 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_MESSAGE): cv.template,
vol.Optional(ATTR_TITLE, default=ATTR_TITLE_DEFAULT): cv.string,
vol.Optional(ATTR_TARGET): cv.string,
vol.Optional(ATTR_DATA): dict, # nobody seems to be using this (yet)
vol.Optional(ATTR_DATA): dict,
})
_LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,62 @@
"""
Join platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.join/
"""
import logging
import voluptuous as vol
from homeassistant.components.notify import (
ATTR_DATA, ATTR_TITLE, BaseNotificationService)
from homeassistant.const import CONF_PLATFORM, CONF_NAME, CONF_API_KEY
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = [
'https://github.com/nkgilley/python-join-api/archive/'
'3e1e849f1af0b4080f551b62270c6d244d5fbcbd.zip#python-join-api==0.0.1']
_LOGGER = logging.getLogger(__name__)
CONF_DEVICE_ID = 'device_id'
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'joaoapps_join',
vol.Required(CONF_DEVICE_ID): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_API_KEY): cv.string
})
# pylint: disable=unused-variable
def get_service(hass, config):
"""Get the Join notification service."""
device_id = config.get(CONF_DEVICE_ID)
api_key = config.get(CONF_API_KEY)
if api_key:
from pyjoin import get_devices
if not get_devices(api_key):
_LOGGER.error("Error connecting to Join, check API key")
return False
return JoinNotificationService(device_id, api_key)
# pylint: disable=too-few-public-methods
class JoinNotificationService(BaseNotificationService):
"""Implement the notification service for Join."""
def __init__(self, device_id, api_key=None):
"""Initialize the service."""
self._device_id = device_id
self._api_key = api_key
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
from pyjoin import send_notification
title = kwargs.get(ATTR_TITLE)
data = kwargs.get(ATTR_DATA) or {}
send_notification(device_id=self._device_id,
text=message,
title=title,
icon=data.get('icon'),
smallicon=data.get('smallicon'),
api_key=self._api_key)

View File

@ -13,3 +13,7 @@ notify:
target:
description: Target of the notification. Optional depending on the platform
example: platform specific
data:
description: Extended information for notification. Optional depending on the platform
example: platform specific

View File

@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import validate_config
REQUIREMENTS = ['slacker==0.9.17']
REQUIREMENTS = ['slacker==0.9.21']
_LOGGER = logging.getLogger(__name__)
@ -30,8 +30,7 @@ def get_service(hass, config):
config[CONF_API_KEY])
except slacker.Error:
_LOGGER.exception(
"Slack authentication failed")
_LOGGER.exception("Slack authentication failed")
return None

View File

@ -4,17 +4,27 @@ Telegram platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.telegram/
"""
import io
import logging
import urllib
import requests
from requests.auth import HTTPBasicAuth
from homeassistant.components.notify import (
ATTR_TITLE, DOMAIN, BaseNotificationService)
ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService)
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-telegram-bot==4.2.1']
REQUIREMENTS = ['python-telegram-bot==4.3.3']
ATTR_PHOTO = "photo"
ATTR_FILE = "file"
ATTR_URL = "url"
ATTR_CAPTION = "caption"
ATTR_USERNAME = "username"
ATTR_PASSWORD = "password"
def get_service(hass, config):
@ -54,9 +64,51 @@ class TelegramNotificationService(BaseNotificationService):
import telegram
title = kwargs.get(ATTR_TITLE)
data = kwargs.get(ATTR_DATA, {})
# send message
try:
self.bot.sendMessage(chat_id=self._chat_id,
text=title + " " + message)
except telegram.error.TelegramError:
_LOGGER.exception("Error sending message.")
return
# send photo
if ATTR_PHOTO in data:
# if not a list
if not isinstance(data[ATTR_PHOTO], list):
photos = [data[ATTR_PHOTO]]
else:
photos = data[ATTR_PHOTO]
try:
for photo_data in photos:
caption = photo_data.get(ATTR_CAPTION, None)
# file is a url
if ATTR_URL in photo_data:
# use http authenticate
if ATTR_USERNAME in photo_data and\
ATTR_PASSWORD in photo_data:
req = requests.get(
photo_data[ATTR_URL],
auth=HTTPBasicAuth(photo_data[ATTR_USERNAME],
photo_data[ATTR_PASSWORD])
)
else:
req = requests.get(photo_data[ATTR_URL])
file_id = io.BytesIO(req.content)
elif ATTR_FILE in photo_data:
file_id = open(photo_data[ATTR_FILE], "rb")
else:
_LOGGER.error("No url or path is set for photo!")
continue
self.bot.sendPhoto(chat_id=self._chat_id,
photo=file_id, caption=caption)
except (OSError, IOError, telegram.error.TelegramError,
urllib.error.HTTPError):
_LOGGER.exception("Error sending photo.")
return

View File

@ -4,6 +4,7 @@ A component which is collecting configuration errors.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/persistent_notification/
"""
import os
import logging
import voluptuous as vol
@ -12,6 +13,7 @@ from homeassistant.exceptions import TemplateError
from homeassistant.helpers import template, config_validation as cv
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.util import slugify
from homeassistant.config import load_yaml_config_file
DOMAIN = 'persistent_notification'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -33,7 +35,7 @@ _LOGGER = logging.getLogger(__name__)
def create(hass, message, title=None, notification_id=None):
"""Turn all or specified light off."""
"""Generate a notification."""
data = {
key: value for key, value in [
(ATTR_TITLE, title),
@ -74,7 +76,10 @@ def setup(hass, config):
hass.states.set(entity_id, message, attr)
hass.services.register(DOMAIN, SERVICE_CREATE, create_service, {},
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_CREATE, create_service,
descriptions[DOMAIN][SERVICE_CREATE],
SCHEMA_SERVICE_CREATE)
return True

View File

@ -1,529 +0,0 @@
"""
Support for recording details.
Component that records all events and state changes. Allows other components
to query this database.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/recorder/
"""
import atexit
import json
import logging
import queue
import sqlite3
import threading
from datetime import date, datetime, timedelta
import voluptuous as vol
import homeassistant.util.dt as dt_util
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
EVENT_TIME_CHANGED, MATCH_ALL)
from homeassistant.core import Event, EventOrigin, State
from homeassistant.remote import JSONEncoder
from homeassistant.helpers.event import track_point_in_utc_time
DOMAIN = "recorder"
DB_FILE = 'home-assistant.db'
RETURN_ROWCOUNT = "rowcount"
RETURN_LASTROWID = "lastrowid"
RETURN_ONE_ROW = "one_row"
CONF_PURGE_DAYS = "purge_days"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
vol.Range(min=1)),
})
}, extra=vol.ALLOW_EXTRA)
_INSTANCE = None
_LOGGER = logging.getLogger(__name__)
def query(sql_query, arguments=None):
"""Query the database."""
_verify_instance()
return _INSTANCE.query(sql_query, arguments)
def query_states(state_query, arguments=None):
"""Query the database and return a list of states."""
return [
row for row in
(row_to_state(row) for row in query(state_query, arguments))
if row is not None]
def query_events(event_query, arguments=None):
"""Query the database and return a list of states."""
return [
row for row in
(row_to_event(row) for row in query(event_query, arguments))
if row is not None]
def row_to_state(row):
"""Convert a database row to a state."""
try:
return State(
row[1], row[2], json.loads(row[3]),
dt_util.utc_from_timestamp(row[4]),
dt_util.utc_from_timestamp(row[5]))
except ValueError:
# When json.loads fails
_LOGGER.exception("Error converting row to state: %s", row)
return None
def row_to_event(row):
"""Convert a databse row to an event."""
try:
return Event(row[1], json.loads(row[2]), EventOrigin(row[3]),
dt_util.utc_from_timestamp(row[5]))
except ValueError:
# When json.loads fails
_LOGGER.exception("Error converting row to event: %s", row)
return None
def run_information(point_in_time=None):
"""Return information about current run.
There is also the run that covers point_in_time.
"""
_verify_instance()
if point_in_time is None or point_in_time > _INSTANCE.recording_start:
return RecorderRun()
run = _INSTANCE.query(
"SELECT * FROM recorder_runs WHERE start<? AND END>?",
(point_in_time, point_in_time), return_value=RETURN_ONE_ROW)
return RecorderRun(run) if run else None
def setup(hass, config):
"""Setup the recorder."""
# pylint: disable=global-statement
global _INSTANCE
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
_INSTANCE = Recorder(hass, purge_days=purge_days)
return True
class RecorderRun(object):
"""Representation of a recorder run."""
def __init__(self, row=None):
"""Initialize the recorder run."""
self.end = None
if row is None:
self.start = _INSTANCE.recording_start
self.closed_incorrect = False
else:
self.start = dt_util.utc_from_timestamp(row[1])
if row[2] is not None:
self.end = dt_util.utc_from_timestamp(row[2])
self.closed_incorrect = bool(row[3])
def entity_ids(self, point_in_time=None):
"""Return the entity ids that existed in this run.
Specify point_in_time if you want to know which existed at that point
in time inside the run.
"""
where = self.where_after_start_run
where_data = []
if point_in_time is not None or self.end is not None:
where += "AND created < ? "
where_data.append(point_in_time or self.end)
return [row[0] for row in query(
"SELECT entity_id FROM states WHERE {}"
"GROUP BY entity_id".format(where), where_data)]
@property
def where_after_start_run(self):
"""Return SQL WHERE clause.
Selection of the rows created after the start of the run.
"""
return "created >= {} ".format(_adapt_datetime(self.start))
@property
def where_limit_to_run(self):
"""Return a SQL WHERE clause.
For limiting the results to this run.
"""
where = self.where_after_start_run
if self.end is not None:
where += "AND created < {} ".format(_adapt_datetime(self.end))
return where
class Recorder(threading.Thread):
"""A threaded recorder class."""
# pylint: disable=too-many-instance-attributes
def __init__(self, hass, purge_days):
"""Initialize the recorder."""
threading.Thread.__init__(self)
self.hass = hass
self.purge_days = purge_days
self.conn = None
self.queue = queue.Queue()
self.quit_object = object()
self.lock = threading.Lock()
self.recording_start = dt_util.utcnow()
self.utc_offset = dt_util.now().utcoffset().total_seconds()
self.db_path = self.hass.config.path(DB_FILE)
def start_recording(event):
"""Start recording."""
self.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_recording)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
hass.bus.listen(MATCH_ALL, self.event_listener)
def run(self):
"""Start processing events to save."""
self._setup_connection()
self._setup_run()
if self.purge_days is not None:
track_point_in_utc_time(self.hass,
lambda now: self._purge_old_data(),
dt_util.utcnow() + timedelta(minutes=5))
while True:
event = self.queue.get()
if event == self.quit_object:
self._close_run()
self._close_connection()
self.queue.task_done()
return
elif event.event_type == EVENT_TIME_CHANGED:
self.queue.task_done()
continue
event_id = self.record_event(event)
if event.event_type == EVENT_STATE_CHANGED:
self.record_state(
event.data['entity_id'], event.data.get('new_state'),
event_id)
self.queue.task_done()
def event_listener(self, event):
"""Listen for new events and put them in the process queue."""
self.queue.put(event)
def shutdown(self, event):
"""Tell the recorder to shut down."""
self.queue.put(self.quit_object)
self.block_till_done()
def record_state(self, entity_id, state, event_id):
"""Save a state to the database."""
now = dt_util.utcnow()
# State got deleted
if state is None:
state_state = ''
state_domain = ''
state_attr = '{}'
last_changed = last_updated = now
else:
state_domain = state.domain
state_state = state.state
state_attr = json.dumps(dict(state.attributes))
last_changed = state.last_changed
last_updated = state.last_updated
info = (
entity_id, state_domain, state_state, state_attr,
last_changed, last_updated,
now, self.utc_offset, event_id)
self.query(
"""
INSERT INTO states (
entity_id, domain, state, attributes, last_changed, last_updated,
created, utc_offset, event_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
info)
def record_event(self, event):
"""Save an event to the database."""
info = (
event.event_type, json.dumps(event.data, cls=JSONEncoder),
str(event.origin), dt_util.utcnow(), event.time_fired,
self.utc_offset
)
return self.query(
"INSERT INTO events ("
"event_type, event_data, origin, created, time_fired, utc_offset"
") VALUES (?, ?, ?, ?, ?, ?)", info, RETURN_LASTROWID)
def query(self, sql_query, data=None, return_value=None):
"""Query the database."""
try:
with self.conn, self.lock:
_LOGGER.debug("Running query %s", sql_query)
cur = self.conn.cursor()
if data is not None:
cur.execute(sql_query, data)
else:
cur.execute(sql_query)
if return_value == RETURN_ROWCOUNT:
return cur.rowcount
elif return_value == RETURN_LASTROWID:
return cur.lastrowid
elif return_value == RETURN_ONE_ROW:
return cur.fetchone()
else:
return cur.fetchall()
except (sqlite3.IntegrityError, sqlite3.OperationalError,
sqlite3.ProgrammingError):
_LOGGER.exception(
"Error querying the database using: %s", sql_query)
return []
def block_till_done(self):
"""Block till all events processed."""
self.queue.join()
def _setup_connection(self):
"""Ensure database is ready to fly."""
self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
self.conn.row_factory = sqlite3.Row
# Make sure the database is closed whenever Python exits
# without the STOP event being fired.
atexit.register(self._close_connection)
# Have datetime objects be saved as integers.
sqlite3.register_adapter(date, _adapt_datetime)
sqlite3.register_adapter(datetime, _adapt_datetime)
# Validate we are on the correct schema or that we have to migrate.
cur = self.conn.cursor()
def save_migration(migration_id):
"""Save and commit a migration to the database."""
cur.execute('INSERT INTO schema_version VALUES (?, ?)',
(migration_id, dt_util.utcnow()))
self.conn.commit()
_LOGGER.info("Database migrated to version %d", migration_id)
try:
cur.execute('SELECT max(migration_id) FROM schema_version;')
migration_id = cur.fetchone()[0] or 0
except sqlite3.OperationalError:
# The table does not exist.
cur.execute('CREATE TABLE schema_version ('
'migration_id integer primary key, performed integer)')
migration_id = 0
if migration_id < 1:
cur.execute("""
CREATE TABLE recorder_runs (
run_id integer primary key,
start integer,
end integer,
closed_incorrect integer default 0,
created integer)
""")
cur.execute("""
CREATE TABLE events (
event_id integer primary key,
event_type text,
event_data text,
origin text,
created integer)
""")
cur.execute(
'CREATE INDEX events__event_type ON events(event_type)')
cur.execute("""
CREATE TABLE states (
state_id integer primary key,
entity_id text,
state text,
attributes text,
last_changed integer,
last_updated integer,
created integer)
""")
cur.execute('CREATE INDEX states__entity_id ON states(entity_id)')
save_migration(1)
if migration_id < 2:
cur.execute("""
ALTER TABLE events
ADD COLUMN time_fired integer
""")
cur.execute('UPDATE events SET time_fired=created')
save_migration(2)
if migration_id < 3:
utc_offset = self.utc_offset
cur.execute("""
ALTER TABLE recorder_runs
ADD COLUMN utc_offset integer
""")
cur.execute("""
ALTER TABLE events
ADD COLUMN utc_offset integer
""")
cur.execute("""
ALTER TABLE states
ADD COLUMN utc_offset integer
""")
cur.execute("UPDATE recorder_runs SET utc_offset=?", [utc_offset])
cur.execute("UPDATE events SET utc_offset=?", [utc_offset])
cur.execute("UPDATE states SET utc_offset=?", [utc_offset])
save_migration(3)
if migration_id < 4:
# We had a bug where we did not save utc offset for recorder runs.
cur.execute(
"""UPDATE recorder_runs SET utc_offset=?
WHERE utc_offset IS NULL""", [self.utc_offset])
cur.execute("""
ALTER TABLE states
ADD COLUMN event_id integer
""")
save_migration(4)
if migration_id < 5:
# Add domain so that thermostat graphs look right.
try:
cur.execute("""
ALTER TABLE states
ADD COLUMN domain text
""")
except sqlite3.OperationalError:
# We had a bug in this migration for a while on dev.
# Without this, dev-users will have to throw away their db.
pass
# TravisCI has Python compiled against an old version of SQLite3
# which misses the instr method.
self.conn.create_function(
"instr", 2,
lambda string, substring: string.find(substring) + 1)
# Populate domain with defaults.
cur.execute("""
UPDATE states
set domain=substr(entity_id, 0, instr(entity_id, '.'))
""")
# Add indexes we are going to use a lot on selects.
cur.execute("""
CREATE INDEX states__state_changes ON
states (last_changed, last_updated, entity_id)""")
cur.execute("""
CREATE INDEX states__significant_changes ON
states (domain, last_updated, entity_id)""")
save_migration(5)
def _close_connection(self):
"""Close connection to the database."""
_LOGGER.info("Closing database")
atexit.unregister(self._close_connection)
self.conn.close()
def _setup_run(self):
"""Log the start of the current run."""
if self.query("""UPDATE recorder_runs SET end=?, closed_incorrect=1
WHERE end IS NULL""", (self.recording_start, ),
return_value=RETURN_ROWCOUNT):
_LOGGER.warning("Found unfinished sessions")
self.query(
"""INSERT INTO recorder_runs (start, created, utc_offset)
VALUES (?, ?, ?)""",
(self.recording_start, dt_util.utcnow(), self.utc_offset))
def _close_run(self):
"""Save end time for current run."""
self.query(
"UPDATE recorder_runs SET end=? WHERE start=?",
(dt_util.utcnow(), self.recording_start))
def _purge_old_data(self):
"""Purge events and states older than purge_days ago."""
if not self.purge_days or self.purge_days < 1:
_LOGGER.debug("purge_days set to %s, will not purge any old data.",
self.purge_days)
return
purge_before = dt_util.utcnow() - timedelta(days=self.purge_days)
_LOGGER.info("Purging events created before %s", purge_before)
deleted_rows = self.query(
sql_query="DELETE FROM events WHERE created < ?;",
data=(int(purge_before.timestamp()),),
return_value=RETURN_ROWCOUNT)
_LOGGER.debug("Deleted %s events", deleted_rows)
_LOGGER.info("Purging states created before %s", purge_before)
deleted_rows = self.query(
sql_query="DELETE FROM states WHERE created < ?;",
data=(int(purge_before.timestamp()),),
return_value=RETURN_ROWCOUNT)
_LOGGER.debug("Deleted %s states", deleted_rows)
# Execute sqlite vacuum command to free up space on disk
self.query("VACUUM;")
def _adapt_datetime(datetimestamp):
"""Turn a datetime into an integer for in the DB."""
return dt_util.as_utc(datetimestamp).timestamp()
def _verify_instance():
"""Throw error if recorder not initialized."""
if _INSTANCE is None:
raise RuntimeError("Recorder not initialized.")

View File

@ -0,0 +1,337 @@
"""
Support for recording details.
Component that records all events and state changes. Allows other components
to query this database.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/recorder/
"""
import logging
import queue
import threading
import time
from datetime import timedelta
import voluptuous as vol
import homeassistant.util.dt as dt_util
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
EVENT_TIME_CHANGED, MATCH_ALL)
from homeassistant.helpers.event import track_point_in_utc_time
DOMAIN = "recorder"
REQUIREMENTS = ['sqlalchemy==1.0.14']
DEFAULT_URL = "sqlite:///{hass_config_path}"
DEFAULT_DB_FILE = "home-assistant_v2.db"
CONF_DB_URL = "db_url"
CONF_PURGE_DAYS = "purge_days"
RETRIES = 3
CONNECT_RETRY_WAIT = 10
QUERY_RETRY_WAIT = 0.1
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
vol.Range(min=1)),
vol.Optional(CONF_DB_URL): vol.Url(''),
})
}, extra=vol.ALLOW_EXTRA)
_INSTANCE = None
_LOGGER = logging.getLogger(__name__)
# These classes will be populated during setup()
# pylint: disable=invalid-name
Session = None
def execute(q):
"""Query the database and convert the objects to HA native form.
This method also retries a few times in the case of stale connections.
"""
import sqlalchemy.exc
for _ in range(0, RETRIES):
try:
return [
row for row in
(row.to_native() for row in q)
if row is not None]
except sqlalchemy.exc.SQLAlchemyError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True)
return []
def run_information(point_in_time=None):
"""Return information about current run.
There is also the run that covers point_in_time.
"""
_verify_instance()
recorder_runs = get_model('RecorderRuns')
if point_in_time is None or point_in_time > _INSTANCE.recording_start:
return recorder_runs(
end=None,
start=_INSTANCE.recording_start,
closed_incorrect=False)
return query('RecorderRuns').filter(
(recorder_runs.start < point_in_time) &
(recorder_runs.end > point_in_time)).first()
def setup(hass, config):
"""Setup the recorder."""
# pylint: disable=global-statement
# pylint: disable=too-many-locals
global _INSTANCE
purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS)
db_url = config.get(DOMAIN, {}).get(CONF_DB_URL, None)
if not db_url:
db_url = DEFAULT_URL.format(
hass_config_path=hass.config.path(DEFAULT_DB_FILE))
_INSTANCE = Recorder(hass, purge_days=purge_days, uri=db_url)
return True
def query(model_name, *args):
"""Helper to return a query handle."""
if isinstance(model_name, str):
return Session().query(get_model(model_name), *args)
return Session().query(model_name, *args)
def get_model(model_name):
"""Get a model class."""
from homeassistant.components.recorder import models
return getattr(models, model_name)
def log_error(e, retry_wait=0, rollback=True,
message="Error during query: %s"):
"""Log about SQLAlchemy errors in a sane manner."""
import sqlalchemy.exc
if not isinstance(e, sqlalchemy.exc.OperationalError):
_LOGGER.exception(e)
else:
_LOGGER.error(message, str(e))
if rollback:
Session().rollback()
if retry_wait:
_LOGGER.info("Retrying failed query in %s seconds", QUERY_RETRY_WAIT)
time.sleep(retry_wait)
class Recorder(threading.Thread):
"""A threaded recorder class."""
# pylint: disable=too-many-instance-attributes
def __init__(self, hass, purge_days, uri):
"""Initialize the recorder."""
threading.Thread.__init__(self)
self.hass = hass
self.purge_days = purge_days
self.queue = queue.Queue()
self.quit_object = object()
self.recording_start = dt_util.utcnow()
self.db_url = uri
self.db_ready = threading.Event()
self.engine = None
self._run = None
def start_recording(event):
"""Start recording."""
self.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_recording)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown)
hass.bus.listen(MATCH_ALL, self.event_listener)
def run(self):
"""Start processing events to save."""
from homeassistant.components.recorder.models import Events, States
import sqlalchemy.exc
global _INSTANCE
while True:
try:
self._setup_connection()
self._setup_run()
break
except sqlalchemy.exc.SQLAlchemyError as e:
log_error(e, retry_wait=CONNECT_RETRY_WAIT, rollback=False,
message="Error during connection setup: %s")
if self.purge_days is not None:
track_point_in_utc_time(self.hass,
lambda now: self._purge_old_data(),
dt_util.utcnow() + timedelta(minutes=5))
while True:
event = self.queue.get()
if event == self.quit_object:
self._close_run()
self._close_connection()
_INSTANCE = None
self.queue.task_done()
return
elif event.event_type == EVENT_TIME_CHANGED:
self.queue.task_done()
continue
session = Session()
dbevent = Events.from_event(event)
session.add(dbevent)
for _ in range(0, RETRIES):
try:
session.commit()
break
except sqlalchemy.exc.OperationalError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT,
rollback=True)
if event.event_type != EVENT_STATE_CHANGED:
self.queue.task_done()
continue
session = Session()
dbstate = States.from_event(event)
for _ in range(0, RETRIES):
try:
dbstate.event_id = dbevent.event_id
session.add(dbstate)
session.commit()
break
except sqlalchemy.exc.OperationalError as e:
log_error(e, retry_wait=QUERY_RETRY_WAIT,
rollback=True)
self.queue.task_done()
def event_listener(self, event):
"""Listen for new events and put them in the process queue."""
self.queue.put(event)
def shutdown(self, event):
"""Tell the recorder to shut down."""
self.queue.put(self.quit_object)
self.queue.join()
def block_till_done(self):
"""Block till all events processed."""
self.queue.join()
def block_till_db_ready(self):
"""Block until the database session is ready."""
self.db_ready.wait()
def _setup_connection(self):
"""Ensure database is ready to fly."""
# pylint: disable=global-statement
global Session
import homeassistant.components.recorder.models as models
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker
if self.db_url == 'sqlite://' or ':memory:' in self.db_url:
from sqlalchemy.pool import StaticPool
self.engine = create_engine(
'sqlite://',
connect_args={'check_same_thread': False},
poolclass=StaticPool)
else:
self.engine = create_engine(self.db_url, echo=False)
models.Base.metadata.create_all(self.engine)
session_factory = sessionmaker(bind=self.engine)
Session = scoped_session(session_factory)
self.db_ready.set()
def _close_connection(self):
"""Close the connection."""
global Session
self.engine.dispose()
self.engine = None
Session = None
def _setup_run(self):
"""Log the start of the current run."""
recorder_runs = get_model('RecorderRuns')
for run in query('RecorderRuns').filter_by(end=None):
run.closed_incorrect = True
run.end = self.recording_start
_LOGGER.warning("Ended unfinished session (id=%s from %s)",
run.run_id, run.start)
Session().add(run)
_LOGGER.warning("Found unfinished sessions")
self._run = recorder_runs(
start=self.recording_start,
created=dt_util.utcnow()
)
session = Session()
session.add(self._run)
session.commit()
def _close_run(self):
"""Save end time for current run."""
self._run.end = dt_util.utcnow()
session = Session()
session.add(self._run)
session.commit()
self._run = None
def _purge_old_data(self):
"""Purge events and states older than purge_days ago."""
from homeassistant.components.recorder.models import Events, States
if not self.purge_days or self.purge_days < 1:
_LOGGER.debug("purge_days set to %s, will not purge any old data.",
self.purge_days)
return
purge_before = dt_util.utcnow() - timedelta(days=self.purge_days)
_LOGGER.info("Purging events created before %s", purge_before)
deleted_rows = Session().query(Events).filter(
(Events.created < purge_before)).delete(synchronize_session=False)
_LOGGER.debug("Deleted %s events", deleted_rows)
_LOGGER.info("Purging states created before %s", purge_before)
deleted_rows = Session().query(States).filter(
(States.created < purge_before)).delete(synchronize_session=False)
_LOGGER.debug("Deleted %s states", deleted_rows)
Session().commit()
Session().expire_all()
# Execute sqlite vacuum command to free up space on disk
if self.engine.driver == 'sqlite':
_LOGGER.info("Vacuuming SQLite to free space")
self.engine.execute("VACUUM")
def _verify_instance():
"""Throw error if recorder not initialized."""
if _INSTANCE is None:
raise RuntimeError("Recorder not initialized.")

View File

@ -0,0 +1,162 @@
"""Models for SQLAlchemy."""
import json
from datetime import datetime
import logging
from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Index, Integer,
String, Text, distinct)
from sqlalchemy.ext.declarative import declarative_base
import homeassistant.util.dt as dt_util
from homeassistant.core import Event, EventOrigin, State
from homeassistant.remote import JSONEncoder
from homeassistant.helpers.entity import split_entity_id
# SQLAlchemy Schema
# pylint: disable=invalid-name
Base = declarative_base()
_LOGGER = logging.getLogger(__name__)
class Events(Base):
# pylint: disable=too-few-public-methods
"""Event history data."""
__tablename__ = 'events'
event_id = Column(Integer, primary_key=True)
event_type = Column(String(32), index=True)
event_data = Column(Text)
origin = Column(String(32))
time_fired = Column(DateTime(timezone=True))
created = Column(DateTime(timezone=True), default=datetime.utcnow)
@staticmethod
def from_event(event):
"""Create an event database object from a native event."""
return Events(event_type=event.event_type,
event_data=json.dumps(event.data, cls=JSONEncoder),
origin=str(event.origin),
time_fired=event.time_fired)
def to_native(self):
"""Convert to a natve HA Event."""
try:
return Event(
self.event_type,
json.loads(self.event_data),
EventOrigin(self.origin),
_process_timestamp(self.time_fired)
)
except ValueError:
# When json.loads fails
_LOGGER.exception("Error converting to event: %s", self)
return None
class States(Base):
# pylint: disable=too-few-public-methods
"""State change history."""
__tablename__ = 'states'
state_id = Column(Integer, primary_key=True)
domain = Column(String(64))
entity_id = Column(String(64))
state = Column(String(255))
attributes = Column(Text)
event_id = Column(Integer, ForeignKey('events.event_id'))
last_changed = Column(DateTime(timezone=True), default=datetime.utcnow)
last_updated = Column(DateTime(timezone=True), default=datetime.utcnow)
created = Column(DateTime(timezone=True), default=datetime.utcnow)
__table_args__ = (Index('states__state_changes',
'last_changed', 'last_updated', 'entity_id'),
Index('states__significant_changes',
'domain', 'last_updated', 'entity_id'), )
@staticmethod
def from_event(event):
"""Create object from a state_changed event."""
entity_id = event.data['entity_id']
state = event.data.get('new_state')
dbstate = States(entity_id=entity_id)
# State got deleted
if state is None:
dbstate.state = ''
dbstate.domain = split_entity_id(entity_id)[0]
dbstate.attributes = '{}'
dbstate.last_changed = event.time_fired
dbstate.last_updated = event.time_fired
else:
dbstate.domain = state.domain
dbstate.state = state.state
dbstate.attributes = json.dumps(dict(state.attributes))
dbstate.last_changed = state.last_changed
dbstate.last_updated = state.last_updated
return dbstate
def to_native(self):
"""Convert to an HA state object."""
try:
return State(
self.entity_id, self.state,
json.loads(self.attributes),
_process_timestamp(self.last_changed),
_process_timestamp(self.last_updated)
)
except ValueError:
# When json.loads fails
_LOGGER.exception("Error converting row to state: %s", self)
return None
class RecorderRuns(Base):
# pylint: disable=too-few-public-methods
"""Representation of recorder run."""
__tablename__ = 'recorder_runs'
run_id = Column(Integer, primary_key=True)
start = Column(DateTime(timezone=True), default=datetime.utcnow)
end = Column(DateTime(timezone=True))
closed_incorrect = Column(Boolean, default=False)
created = Column(DateTime(timezone=True), default=datetime.utcnow)
def entity_ids(self, point_in_time=None):
"""Return the entity ids that existed in this run.
Specify point_in_time if you want to know which existed at that point
in time inside the run.
"""
from sqlalchemy.orm.session import Session
session = Session.object_session(self)
assert session is not None, 'RecorderRuns need to be persisted'
query = session.query(distinct(States.entity_id)).filter(
States.last_updated >= self.start)
if point_in_time is not None:
query = query.filter(States.last_updated < point_in_time)
elif self.end is not None:
query = query.filter(States.last_updated < self.end)
return [row[0] for row in query]
def to_native(self):
"""Return self, native format is this model."""
return self
def _process_timestamp(ts):
"""Process a timestamp into datetime object."""
if ts is None:
return None
elif ts.tzinfo is None:
return dt_util.UTC.localize(ts)
else:
return dt_util.as_utc(ts)

View File

@ -14,7 +14,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity
from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
REQUIREMENTS = ['pyRFXtrx==0.8.0']
REQUIREMENTS = ['pyRFXtrx==0.9.0']
DOMAIN = "rfxtrx"
@ -40,6 +40,7 @@ DATA_TYPES = OrderedDict([
('Rain rate', ''),
('Energy usage', 'W'),
('Total usage', 'W'),
('Sound', ''),
('Sensor Status', ''),
('Unknown', '')])
@ -65,6 +66,9 @@ def _valid_device(value, device_type):
key = device.get('packetid')
device.pop('packetid')
if not len(key) % 2 == 0:
key = '0' + key
if get_rfx_object(key) is None:
raise vol.Invalid('Rfxtrx device {} is invalid: '
'Invalid device id for {}'.format(key, value))
@ -159,7 +163,11 @@ def get_rfx_object(packetid):
"""Return the RFXObject with the packetid."""
import RFXtrx as rfxtrxmod
try:
binarypacket = bytearray.fromhex(packetid)
except ValueError:
return None
pkt = rfxtrxmod.lowlevel.parse(binarypacket)
if pkt is None:
return None

View File

@ -10,7 +10,7 @@ from homeassistant.components.rollershutter import RollershutterDevice
from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -11,33 +11,109 @@ from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
DEPENDENCIES = [apcupsd.DOMAIN]
DEFAULT_NAME = "UPS Status"
SENSOR_PREFIX = 'UPS '
SENSOR_TYPES = {
'alarmdel': ['Alarm Delay', '', 'mdi:alarm'],
'ambtemp': ['Ambient Temperature', '', 'mdi:thermometer'],
'apc': ['Status Data', '', 'mdi:information-outline'],
'apcmodel': ['Model', '', 'mdi:information-outline'],
'badbatts': ['Bad Batteries', '', 'mdi:information-outline'],
'battdate': ['Battery Replaced', '', 'mdi:calendar-clock'],
'battstat': ['Battery Status', '', 'mdi:information-outline'],
'battv': ['Battery Voltage', 'V', 'mdi:flash'],
'bcharge': ['Battery', '%', 'mdi:battery'],
'cable': ['Cable Type', '', 'mdi:ethernet-cable'],
'cumonbatt': ['Total Time on Battery', '', 'mdi:timer'],
'date': ['Status Date', '', 'mdi:calendar-clock'],
'dipsw': ['Dip Switch Settings', '', 'mdi:information-outline'],
'dlowbatt': ['Low Battery Signal', '', 'mdi:clock-alert'],
'driver': ['Driver', '', 'mdi:information-outline'],
'dshutd': ['Shutdown Delay', '', 'mdi:timer'],
'dwake': ['Wake Delay', '', 'mdi:timer'],
'endapc': ['Date and Time', '', 'mdi:calendar-clock'],
'extbatts': ['External Batteries', '', 'mdi:information-outline'],
'firmware': ['Firmware Version', '', 'mdi:information-outline'],
'hitrans': ['Transfer High', 'V', 'mdi:flash'],
'hostname': ['Hostname', '', 'mdi:information-outline'],
'humidity': ['Ambient Humidity', '%', 'mdi:water-percent'],
'itemp': ['Internal Temperature', TEMP_CELSIUS, 'mdi:thermometer'],
'lastxfer': ['Last Transfer', '', 'mdi:transfer'],
'linefail': ['Input Voltage Status', '', 'mdi:information-outline'],
'linefreq': ['Line Frequency', 'Hz', 'mdi:information-outline'],
'linev': ['Input Voltage', 'V', 'mdi:flash'],
'loadpct': ['Load', '%', 'mdi:gauge'],
'lotrans': ['Transfer Low', 'V', 'mdi:flash'],
'mandate': ['Manufacture Date', '', 'mdi:calendar'],
'masterupd': ['Master Update', '', 'mdi:information-outline'],
'maxlinev': ['Input Voltage High', 'V', 'mdi:flash'],
'maxtime': ['Battery Timeout', '', 'mdi:timer-off'],
'mbattchg': ['Battery Shutdown', '%', 'mdi:battery-alert'],
'minlinev': ['Input Voltage Low', 'V', 'mdi:flash'],
'mintimel': ['Shutdown Time', '', 'mdi:timer'],
'model': ['Model', '', 'mdi:information-outline'],
'nombattv': ['Battery Nominal Voltage', 'V', 'mdi:flash'],
'nominv': ['Nominal Input Voltage', 'V', 'mdi:flash'],
'nomoutv': ['Nominal Output Voltage', 'V', 'mdi:flash'],
'nompower': ['Nominal Output Power', 'W', 'mdi:flash'],
'numxfers': ['Transfer Count', '', 'mdi:counter'],
'outputv': ['Output Voltage', 'V', 'mdi:flash'],
'reg1': ['Register 1 Fault', '', 'mdi:information-outline'],
'reg2': ['Register 2 Fault', '', 'mdi:information-outline'],
'reg3': ['Register 3 Fault', '', 'mdi:information-outline'],
'retpct': ['Restore Requirement', '%', 'mdi:battery-alert'],
'selftest': ['Last Self Test', '', 'mdi:calendar-clock'],
'sense': ['Sensitivity', '', 'mdi:information-outline'],
'serialno': ['Serial Number', '', 'mdi:information-outline'],
'starttime': ['Startup Time', '', 'mdi:calendar-clock'],
'statflag': ['Status Flag', '', 'mdi:information-outline'],
'status': ['Status', '', 'mdi:information-outline'],
'stesti': ['Self Test Interval', '', 'mdi:information-outline'],
'timeleft': ['Time Left', '', 'mdi:clock-alert'],
'tonbatt': ['Time on Battery', '', 'mdi:timer'],
'upsmode': ['Mode', '', 'mdi:information-outline'],
'upsname': ['Name', '', 'mdi:information-outline'],
'version': ['Daemon Info', '', 'mdi:information-outline'],
'xoffbat': ['Transfer from Battery', '', 'mdi:transfer'],
'xoffbatt': ['Transfer from Battery', '', 'mdi:transfer'],
'xonbatt': ['Transfer to Battery', '', 'mdi:transfer'],
}
SPECIFIC_UNITS = {
"ITEMP": TEMP_CELSIUS
'ITEMP': TEMP_CELSIUS
}
INFERRED_UNITS = {
' Minutes': 'min',
' Seconds': 'sec',
' Percent': '%',
' Volts': 'V',
' Watts': 'W',
' Hz': 'Hz',
' C': TEMP_CELSIUS,
}
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Setup the APCUPSd sensor."""
typ = config.get(apcupsd.CONF_TYPE)
if typ is None:
_LOGGER.error(
"You must include a '%s' when configuring an APCUPSd sensor.",
apcupsd.CONF_TYPE)
return False
typ = typ.upper()
"""Setup the APCUPSd sensors."""
entities = []
if typ not in apcupsd.DATA.status:
_LOGGER.error(
"Specified '%s' of '%s' does not appear in the APCUPSd status "
"output.", apcupsd.CONF_TYPE, typ)
return False
for resource in config['resources']:
sensor_type = resource.lower()
add_entities((
Sensor(config, apcupsd.DATA, unit=SPECIFIC_UNITS.get(typ)),
))
if sensor_type not in SENSOR_TYPES:
SENSOR_TYPES[sensor_type] = [
sensor_type.title(), '', 'mdi:information-outline']
if sensor_type.upper() not in apcupsd.DATA.status:
_LOGGER.warning(
'Sensor type: "%s" does not appear in the APCUPSd status '
'output.', sensor_type)
entities.append(APCUPSdSensor(apcupsd.DATA, sensor_type))
add_entities(entities)
def infer_unit(value):
@ -49,25 +125,31 @@ def infer_unit(value):
from apcaccess.status import ALL_UNITS
for unit in ALL_UNITS:
if value.endswith(unit):
return value[:-len(unit)], unit
return value[:-len(unit)], INFERRED_UNITS.get(unit, unit.strip())
return value, None
class Sensor(Entity):
class APCUPSdSensor(Entity):
"""Representation of a sensor entity for APCUPSd status values."""
def __init__(self, config, data, unit=None):
def __init__(self, data, sensor_type):
"""Initialize the sensor."""
self._config = config
self._unit = unit
self._data = data
self.type = sensor_type
self._name = SENSOR_PREFIX + SENSOR_TYPES[sensor_type][0]
self._unit = SENSOR_TYPES[sensor_type][1]
self._inferred_unit = None
self.update()
@property
def name(self):
"""Return the name of the UPS sensor."""
return self._config.get("name", DEFAULT_NAME)
return self._name
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return SENSOR_TYPES[self.type][2]
@property
def state(self):
@ -77,11 +159,15 @@ class Sensor(Entity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
if self._unit is None:
if not self._unit:
return self._inferred_unit
return self._unit
def update(self):
"""Get the latest status and use it to update our sensor state."""
key = self._config[apcupsd.CONF_TYPE].upper()
self._state, self._inferred_unit = infer_unit(self._data.status[key])
if self.type.upper() not in self._data.status:
self._state = None
self._inferred_unit = None
else:
self._state, self._inferred_unit = infer_unit(
self._data.status[self.type.upper()])

View File

@ -13,6 +13,7 @@ from homeassistant.const import (CONF_PLATFORM)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['schiene==0.17']
@ -88,6 +89,7 @@ class SchieneData(object):
def __init__(self, start, goal):
"""Initialize the sensor."""
import schiene
self.start = start
self.goal = goal
self.schiene = schiene.Schiene()
@ -96,7 +98,8 @@ class SchieneData(object):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update the connection data."""
self.connections = self.schiene.connections(self.start, self.goal)
self.connections = self.schiene.connections(
self.start, self.goal, dt_util.as_local(dt_util.utcnow()))
for con in self.connections:
# Detail info is not useful. Having a more consistent interface

View File

@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class EnvisalinkSensor(EnvisalinkDevice):
"""Representation of an envisalink keypad."""
"""Representation of an Envisalink keypad."""
def __init__(self, partition_name, partition_number, info, controller):
"""Initialize the sensor."""

View File

@ -10,14 +10,18 @@ import logging
import voluptuous as vol
from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.const import (
CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE)
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.location as location
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['googlemaps==2.4.3']
REQUIREMENTS = ['googlemaps==2.4.4']
# Return cached results if last update was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
@ -65,6 +69,8 @@ PLATFORM_SCHEMA = vol.Schema({
}))
})
TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone"]
def convert_time_to_utc(timestr):
"""Take a string like 08:00:00 and convert it to a unix timestamp."""
@ -78,6 +84,11 @@ def convert_time_to_utc(timestr):
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the travel time platform."""
# pylint: disable=too-many-locals
def run_setup(event):
"""Delay the setup until home assistant is fully initialized.
This allows any entities to be created already
"""
options = config.get(CONF_OPTIONS)
if options.get('units') is None:
@ -90,8 +101,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
mode = options.get(CONF_MODE)
if travel_mode is not None:
wstr = ("Google Travel Time: travel_mode is deprecated, please add "
"mode to the options dictionary instead!")
wstr = ("Google Travel Time: travel_mode is deprecated, please "
"add mode to the options dictionary instead!")
_LOGGER.warning(wstr)
if mode is None:
options[CONF_MODE] = travel_mode
@ -103,27 +114,40 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
origin = config.get(CONF_ORIGIN)
destination = config.get(CONF_DESTINATION)
sensor = GoogleTravelTimeSensor(name, api_key, origin, destination,
options)
sensor = GoogleTravelTimeSensor(hass, name, api_key, origin,
destination, options)
if sensor.valid_api_connection:
add_devices_callback([sensor])
# Wait until start event is sent to load this component.
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup)
# pylint: disable=too-many-instance-attributes
class GoogleTravelTimeSensor(Entity):
"""Representation of a tavel time sensor."""
# pylint: disable=too-many-arguments
def __init__(self, name, api_key, origin, destination, options):
def __init__(self, hass, name, api_key, origin, destination, options):
"""Initialize the sensor."""
self._hass = hass
self._name = name
self._options = options
self._origin = origin
self._destination = destination
self._matrix = None
self.valid_api_connection = True
# Check if location is a trackable entity
if origin.split('.', 1)[0] in TRACKABLE_DOMAINS:
self._origin_entity_id = origin
else:
self._origin = origin
if destination.split('.', 1)[0] in TRACKABLE_DOMAINS:
self._destination_entity_id = destination
else:
self._destination = destination
import googlemaps
self._client = googlemaps.Client(api_key, timeout=10)
try:
@ -136,6 +160,9 @@ class GoogleTravelTimeSensor(Entity):
@property
def state(self):
"""Return the state of the sensor."""
if self._matrix is None:
return None
_data = self._matrix['rows'][0]['elements'][0]
if 'duration_in_traffic' in _data:
return round(_data['duration_in_traffic']['value']/60)
@ -151,6 +178,9 @@ class GoogleTravelTimeSensor(Entity):
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._matrix is None:
return None
res = self._matrix.copy()
res.update(self._options)
del res['rows']
@ -186,6 +216,64 @@ class GoogleTravelTimeSensor(Entity):
elif atime is not None:
options_copy['arrival_time'] = atime
# Convert device_trackers to google friendly location
if hasattr(self, '_origin_entity_id'):
self._origin = self._get_location_from_entity(
self._origin_entity_id
)
if hasattr(self, '_destination_entity_id'):
self._destination = self._get_location_from_entity(
self._destination_entity_id
)
self._destination = self._resolve_zone(self._destination)
self._origin = self._resolve_zone(self._origin)
if self._destination is not None and self._origin is not None:
self._matrix = self._client.distance_matrix(self._origin,
self._destination,
**options_copy)
def _get_location_from_entity(self, entity_id):
"""Get the location from the entity state or attributes."""
entity = self._hass.states.get(entity_id)
if entity is None:
_LOGGER.error("Unable to find entity %s", entity_id)
self.valid_api_connection = False
return None
# Check if device is in a zone
zone_entity = self._hass.states.get("zone.%s" % entity.state)
if location.has_location(zone_entity):
_LOGGER.debug(
"%s is in %s, getting zone location.",
entity_id, zone_entity.entity_id
)
return self._get_location_from_attributes(zone_entity)
# If zone was not found in state then use the state as the location
if entity_id.startswith("sensor."):
return entity.state
# For everything else look for location attributes
if location.has_location(entity):
return self._get_location_from_attributes(entity)
# When everything fails just return nothing
return None
@staticmethod
def _get_location_from_attributes(entity):
"""Get the lat/long string from an entities attributes."""
attr = entity.attributes
return "%s,%s" % (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE))
def _resolve_zone(self, friendly_name):
entities = self._hass.states.all()
for entity in entities:
if entity.domain == 'zone' and entity.name == friendly_name:
return self._get_location_from_attributes(entity)
return friendly_name

View File

@ -28,7 +28,10 @@ HM_UNIT_HA_CAST = {
"POWER": "W",
"CURRENT": "mA",
"VOLTAGE": "V",
"ENERGY_COUNTER": "Wh"
"ENERGY_COUNTER": "Wh",
"GAS_POWER": "m3",
"GAS_ENERGY_COUNTER": "m3",
"LUX": "lux"
}

View File

@ -0,0 +1,105 @@
"""
IMAP sensor support.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.imap/
"""
import logging
import voluptuous as vol
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
ICON = 'mdi:email-outline'
CONF_USER = "user"
CONF_PASSWORD = "password"
CONF_SERVER = "server"
CONF_PORT = "port"
CONF_NAME = "name"
DEFAULT_PORT = 993
PLATFORM_SCHEMA = vol.Schema({
vol.Required('platform'): 'imap',
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_USER): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_SERVER): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT):
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the IMAP platform."""
sensor = ImapSensor(config.get(CONF_NAME, None),
config.get(CONF_USER),
config.get(CONF_PASSWORD),
config.get(CONF_SERVER),
config.get(CONF_PORT, DEFAULT_PORT))
if sensor.connection:
add_devices([sensor])
else:
return False
class ImapSensor(Entity):
"""Representation of an IMAP sensor."""
# pylint: disable=too-many-arguments
def __init__(self, name, user, password, server, port):
"""Initialize the sensor."""
self._name = name or user
self._user = user
self._password = password
self._server = server
self._port = port
self._unread_count = 0
self.connection = self._login()
self.update()
def _login(self):
"""Login and return an IMAP connection."""
import imaplib
try:
connection = imaplib.IMAP4_SSL(self._server, self._port)
connection.login(self._user, self._password)
return connection
except imaplib.IMAP4.error:
_LOGGER.error("Failed to login to %s.", self._server)
return False
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the number of unread emails."""
return self._unread_count
def update(self):
"""Check the number of unread emails."""
import imaplib
try:
self.connection.select()
self._unread_count = len(self.connection.search(
None, 'UnSeen')[1][0].split())
except imaplib.IMAP4.abort:
_LOGGER.info("Connection to %s lost, attempting to reconnect",
self._server)
try:
self._login()
self.update()
except imaplib.IMAP4.error:
_LOGGER.error("Failed to reconnect.")
@property
def icon(self):
"""Return the icon to use in the frontend."""
return ICON

View File

@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = "loopenergy"
REQUIREMENTS = ['pyloopenergy==0.0.13']
REQUIREMENTS = ['pyloopenergy==0.0.14']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -15,7 +15,7 @@ DEPENDENCIES = []
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the mysensors platform for sensors."""
"""Setup the MySensors platform for sensors."""
# Only act if loaded via mysensors by discovery event.
# Otherwise gateway is not setup.
if discovery_info is None:
@ -72,7 +72,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity):
"""Represent the value of a MySensors Sensor child node."""
"""Representation of a MySensors Sensor child node."""
@property
def state(self):

View File

@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = ['pyowm==2.3.1']
REQUIREMENTS = ['pyowm==2.3.2']
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
'weather': ['Condition', None],
@ -127,9 +127,9 @@ class OpenWeatherMapSensor(Entity):
else:
self._state = round(data.get_temperature()['temp'], 1)
elif self.type == 'wind_speed':
self._state = data.get_wind()['speed']
self._state = round(data.get_wind()['speed'], 1)
elif self.type == 'humidity':
self._state = data.get_humidity()
self._state = round(data.get_humidity(), 1)
elif self.type == 'pressure':
self._state = round(data.get_pressure()['press'], 0)
elif self.type == 'clouds':

View File

@ -41,12 +41,13 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
sub_sensors = {}
data_types = entity_info[ATTR_DATA_TYPE]
if len(data_types) == 0:
data_type = "Unknown"
for data_type in DATA_TYPES:
if data_type in event.values:
data_types = [data_type]
break
for _data_type in data_types:
new_sensor = RfxtrxSensor(event, entity_info[ATTR_NAME],
new_sensor = RfxtrxSensor(None, entity_info[ATTR_NAME],
_data_type)
sensors.append(new_sensor)
sub_sensors[_data_type] = new_sensor
@ -109,7 +110,7 @@ class RfxtrxSensor(Entity):
@property
def state(self):
"""Return the state of the sensor."""
if self.data_type:
if self.event:
return self.event.values[self.data_type]
return None
@ -121,6 +122,7 @@ class RfxtrxSensor(Entity):
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self.event:
return self.event.values
@property

View File

@ -5,26 +5,28 @@ 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)
from homeassistant.const import (TEMP_CELSIUS, CONF_PLATFORM, CONF_NAME,
STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = ['beautifulsoup4==4.4.1']
REQUIREMENTS = ['xmltodict==0.10.2']
_LOGGER = logging.getLogger(__name__)
_RESOURCE = 'http://www.hydrodaten.admin.ch/en/'
_RESOURCE = 'http://www.hydrodata.ch/xml/SMS.xml'
DEFAULT_NAME = 'Water temperature'
CONF_STATION = 'station'
ICON = 'mdi:cup-water'
ATTR_LOCATION = 'Location'
ATTR_UPDATE = 'Update'
ATTR_DISCHARGE = 'Discharge'
ATTR_WATERLEVEL = 'Level'
ATTR_DISCHARGE_MEAN = 'Discharge mean'
@ -37,30 +39,25 @@ 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,
vol.Required(CONF_STATION): vol.Coerce(int),
})
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(minutes=30)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Swiss hydrological sensor."""
import xmltodict
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)
response = requests.get(_RESOURCE, timeout=5)
if any(str(station) == location.get('@StrNr') for location in
xmltodict.parse(response.text)['AKT_Data']['MesPar']) is False:
_LOGGER.error('The given station does not exist: %s', station)
return False
except requests.exceptions.ConnectionError:
_LOGGER.error('The URL is not accessible')
@ -89,27 +86,47 @@ class SwissHydrologicalDataSensor(Entity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
if self._state is not STATE_UNKNOWN:
return self._unit_of_measurement
else:
return None
@property
def state(self):
"""Return the state of the sensor."""
return self._state
try:
return round(float(self._state), 1)
except ValueError:
return STATE_UNKNOWN
@property
def device_state_attributes(self):
"""Return the state attributes."""
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,
}
if '02' in self.data.measurings:
attributes[ATTR_WATERLEVEL] = self.data.measurings['02'][
'current']
attributes[ATTR_WATERLEVEL_MEAN] = self.data.measurings['02'][
'mean']
attributes[ATTR_WATERLEVEL_MAX] = self.data.measurings['02'][
'max']
if '03' in self.data.measurings:
attributes[ATTR_TEMPERATURE_MEAN] = self.data.measurings['03'][
'mean']
attributes[ATTR_TEMPERATURE_MAX] = self.data.measurings['03'][
'max']
if '10' in self.data.measurings:
attributes[ATTR_DISCHARGE] = self.data.measurings['10'][
'current']
attributes[ATTR_DISCHARGE_MEAN] = self.data.measurings['10'][
'current']
attributes[ATTR_DISCHARGE_MAX] = self.data.measurings['10'][
'max']
attributes[ATTR_LOCATION] = self.data.measurings['location']
attributes[ATTR_UPDATE] = self.data.measurings['update_time']
return attributes
@property
def icon(self):
@ -121,7 +138,10 @@ class SwissHydrologicalDataSensor(Entity):
"""Get the latest data and update the states."""
self.data.update()
if self.data.measurings is not None:
self._state = self.data.measurings.temperature
if '03' not in self.data.measurings:
self._state = STATE_UNKNOWN
else:
self._state = self.data.measurings['03']['current']
# pylint: disable=too-few-public-methods
@ -135,29 +155,34 @@ class HydrologicalData(object):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from hydrodaten.admin.ch."""
from bs4 import BeautifulSoup
"""Get the latest data from hydrodata.ch."""
import xmltodict
details = {}
try:
response = requests.get('{}/{}.html'.format(_RESOURCE,
self.station),
timeout=5)
response = requests.get(_RESOURCE, timeout=5)
except requests.exceptions.ConnectionError:
_LOGGER.error('Unable to retrieve data')
response = None
_LOGGER.error('Unable to retrieve data from %s', _RESOURCE)
try:
tables = BeautifulSoup(response.content,
'html.parser').findChildren('table')
rows = tables[0].findChildren(['th', 'tr'])
stations = xmltodict.parse(response.text)['AKT_Data']['MesPar']
# Water level: Typ="02", temperature: Typ="03", discharge: Typ="10"
for station in stations:
if str(self.station) != station.get('@StrNr'):
continue
for data in ['02', '03', '10']:
if data != station.get('@Typ'):
continue
values = station.get('Wert')
if values is not None:
details[data] = {
'current': values[0],
'max': list(values[4].items())[1][1],
'mean': list(values[3].items())[1][1]}
details = []
details['location'] = station.get('Name')
details['update_time'] = station.get('Zeit')
for row in rows:
cells = row.findChildren('td')
for cell in cells:
details.append(cell.string)
self.measurings = HydroData._make(details)
self.measurings = details
except AttributeError:
self.measurings = None

View File

@ -64,7 +64,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
continue
sensor_name = str(ts_sensor.id)
for datatype in sensor_value_descriptions.keys():
for datatype in sensor_value_descriptions:
if datatype & datatype_mask and ts_sensor.has_value(datatype):
sensor_info = sensor_value_descriptions[datatype]

View File

@ -68,7 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = []
for device in devices:
for type_name in SENSOR_TYPES.keys():
for type_name in SENSOR_TYPES:
dev.append(ThinkingCleanerSensor(device, type_name,
update_devices))

View File

@ -11,9 +11,9 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['uber_rides==0.2.1']
REQUIREMENTS = ["uber_rides==0.2.4"]
ICON = 'mdi:taxi'
ICON = "mdi:taxi"
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
@ -21,35 +21,35 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Uber sensor."""
if None in (config.get('start_latitude'), config.get('start_longitude')):
if None in (config.get("start_latitude"), config.get("start_longitude")):
_LOGGER.error(
"You must set start latitude and longitude to use the Uber sensor!"
)
return False
if config.get('server_token') is None:
if config.get("server_token") is None:
_LOGGER.error("You must set a server_token to use the Uber sensor!")
return False
from uber_rides.session import Session
session = Session(server_token=config.get('server_token'))
session = Session(server_token=config.get("server_token"))
wanted_product_ids = config.get('product_ids')
wanted_product_ids = config.get("product_ids")
dev = []
timeandpriceest = UberEstimate(session, config['start_latitude'],
config['start_longitude'],
config.get('end_latitude'),
config.get('end_longitude'))
timeandpriceest = UberEstimate(session, config["start_latitude"],
config["start_longitude"],
config.get("end_latitude"),
config.get("end_longitude"))
for product_id, product in timeandpriceest.products.items():
if (wanted_product_ids is not None) and \
(product_id not in wanted_product_ids):
continue
dev.append(UberSensor('time', timeandpriceest, product_id, product))
is_metered = (product['price_details']['estimate'] == "Metered")
if 'price_details' in product and is_metered is False:
dev.append(UberSensor('price', timeandpriceest,
dev.append(UberSensor("time", timeandpriceest, product_id, product))
if (product.get("price_details") is not None) and \
product["price_details"]["estimate"] is not "Metered":
dev.append(UberSensor("price", timeandpriceest,
product_id, product))
add_devices(dev)
@ -64,30 +64,30 @@ class UberSensor(Entity):
self._product_id = product_id
self._product = product
self._sensortype = sensorType
self._name = "{} {}".format(self._product['display_name'],
self._name = "{} {}".format(self._product["display_name"],
self._sensortype)
if self._sensortype == "time":
self._unit_of_measurement = "min"
time_estimate = self._product.get('time_estimate_seconds', 0)
time_estimate = self._product.get("time_estimate_seconds", 0)
self._state = int(time_estimate / 60)
elif self._sensortype == "price":
if 'price_details' in self._product:
price_details = self._product['price_details']
self._unit_of_measurement = price_details.get('currency_code',
'N/A')
if 'low_estimate' in price_details:
statekey = 'minimum'
if self._product.get("price_details") is not None:
price_details = self._product["price_details"]
self._unit_of_measurement = price_details.get("currency_code")
if price_details.get("low_estimate") is not None:
statekey = "minimum"
else:
statekey = 'low_estimate'
statekey = "low_estimate"
self._state = int(price_details.get(statekey, 0))
else:
self._unit_of_measurement = 'N/A'
self._state = 0
self.update()
@property
def name(self):
"""Return the name of the sensor."""
if "uber" not in self._name.lower():
self._name = "Uber{}".format(self._name)
return self._name
@property
@ -103,43 +103,41 @@ class UberSensor(Entity):
@property
def device_state_attributes(self):
"""Return the state attributes."""
time_estimate = self._product.get('time_estimate_seconds', 'N/A')
time_estimate = self._product.get("time_estimate_seconds")
params = {
'Product ID': self._product['product_id'],
'Product short description': self._product['short_description'],
'Product display name': self._product['display_name'],
'Product description': self._product['description'],
'Pickup time estimate (in seconds)': time_estimate,
'Trip duration (in seconds)': self._product.get('duration', 'N/A'),
'Vehicle Capacity': self._product['capacity']
"Product ID": self._product["product_id"],
"Product short description": self._product["short_description"],
"Product display name": self._product["display_name"],
"Product description": self._product["description"],
"Pickup time estimate (in seconds)": time_estimate,
"Trip duration (in seconds)": self._product.get("duration"),
"Vehicle Capacity": self._product["capacity"]
}
if 'price_details' in self._product:
price_details = self._product['price_details']
distance_key = 'Trip distance (in {}s)'.format(price_details[
'distance_unit'])
distance_val = self._product.get('distance')
params['Minimum price'] = price_details['minimum'],
params['Cost per minute'] = price_details['cost_per_minute'],
params['Distance units'] = price_details['distance_unit'],
params['Cancellation fee'] = price_details['cancellation_fee'],
params['Cost per distance'] = price_details['cost_per_distance'],
params['Base price'] = price_details['base'],
params['Price estimate'] = price_details.get('estimate', 'N/A'),
params['Price currency code'] = price_details.get('currency_code'),
params['High price estimate'] = price_details.get('high_estimate',
'N/A'),
params['Low price estimate'] = price_details.get('low_estimate',
'N/A'),
params['Surge multiplier'] = price_details.get('surge_multiplier',
'N/A')
if self._product.get("price_details") is not None:
price_details = self._product["price_details"]
dunit = price_details.get("distance_unit")
distance_key = "Trip distance (in {}s)".format(dunit)
distance_val = self._product.get("distance")
params["Cost per minute"] = price_details.get("cost_per_minute")
params["Distance units"] = price_details.get("distance_unit")
params["Cancellation fee"] = price_details.get("cancellation_fee")
cpd = price_details.get("cost_per_distance")
params["Cost per distance"] = cpd
params["Base price"] = price_details.get("base")
params["Minimum price"] = price_details.get("minimum")
params["Price estimate"] = price_details.get("estimate")
params["Price currency code"] = price_details.get("currency_code")
params["High price estimate"] = price_details.get("high_estimate")
params["Low price estimate"] = price_details.get("low_estimate")
params["Surge multiplier"] = price_details.get("surge_multiplier")
else:
distance_key = 'Trip distance (in miles)'
distance_val = self._product.get('distance', 'N/A')
distance_key = "Trip distance (in miles)"
distance_val = self._product.get("distance")
params[distance_key] = distance_val
return params
return {k: v for k, v in params.items() if v is not None}
@property
def icon(self):
@ -152,13 +150,13 @@ class UberSensor(Entity):
self.data.update()
self._product = self.data.products[self._product_id]
if self._sensortype == "time":
time_estimate = self._product.get('time_estimate_seconds', 0)
time_estimate = self._product.get("time_estimate_seconds", 0)
self._state = int(time_estimate / 60)
elif self._sensortype == "price":
price_details = self._product.get('price_details')
price_details = self._product.get("price_details")
if price_details is not None:
min_price = price_details.get('minimum')
self._state = int(price_details.get('low_estimate', min_price))
min_price = price_details.get("minimum")
self._state = int(price_details.get("low_estimate", min_price))
else:
self._state = 0
@ -190,40 +188,39 @@ class UberEstimate(object):
products_response = client.get_products(
self.start_latitude, self.start_longitude)
products = products_response.json.get('products')
products = products_response.json.get("products")
for product in products:
self.products[product['product_id']] = product
self.products[product["product_id"]] = product
if self.end_latitude is not None and self.end_longitude is not None:
price_response = client.get_price_estimates(
self.start_latitude,
self.start_longitude,
self.end_latitude,
self.end_longitude)
self.start_latitude, self.start_longitude,
self.end_latitude, self.end_longitude)
prices = price_response.json.get('prices', [])
prices = price_response.json.get("prices", [])
for price in prices:
product = self.products[price['product_id']]
product = self.products[price["product_id"]]
product["duration"] = price.get("duration", "0")
product["distance"] = price.get("distance", "0")
price_details = product.get("price_details")
product["duration"] = price.get('duration', '0')
product["distance"] = price.get('distance', '0')
if price_details is not None:
price_details["estimate"] = price.get('estimate',
'0')
price_details["high_estimate"] = price.get('high_estimate',
'0')
price_details["low_estimate"] = price.get('low_estimate',
'0')
surge_multiplier = price.get('surge_multiplier', '0')
if product.get("price_details") is None:
price_details = {}
price_details["estimate"] = price.get("estimate", "0")
price_details["high_estimate"] = price.get("high_estimate",
"0")
price_details["low_estimate"] = price.get("low_estimate", "0")
price_details["currency_code"] = price.get("currency_code")
surge_multiplier = price.get("surge_multiplier", "0")
price_details["surge_multiplier"] = surge_multiplier
product["price_details"] = price_details
estimate_response = client.get_pickup_time_estimates(
self.start_latitude, self.start_longitude)
estimates = estimate_response.json.get('times')
estimates = estimate_response.json.get("times")
for estimate in estimates:
self.products[estimate['product_id']][
"time_estimate_seconds"] = estimate.get('estimate', '0')
self.products[estimate["product_id"]][
"time_estimate_seconds"] = estimate.get("estimate", "0")

View File

@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.components.wink import WinkDevice
from homeassistant.loader import get_component
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
SENSOR_TYPES = ['temperature', 'humidity']

View File

@ -18,8 +18,7 @@ from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['xmltodict']
REQUIREMENTS = ['xmltodict==0.10.2']
# Sensor types are defined like so:
SENSOR_TYPES = {

View File

@ -0,0 +1,189 @@
"""
Support for the Yahoo! Weather service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.yweather/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.const import (CONF_PLATFORM, TEMP_CELSIUS,
CONF_MONITORED_CONDITIONS, STATE_UNKNOWN)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = ["yahooweather==0.4"]
SENSOR_TYPES = {
'weather_current': ['Current', None],
'weather': ['Condition', None],
'temperature': ['Temperature', "temperature"],
'temp_min': ['Temperature', "temperature"],
'temp_max': ['Temperature', "temperature"],
'wind_speed': ['Wind speed', "speed"],
'humidity': ['Humidity', "%"],
'pressure': ['Pressure', "pressure"],
'visibility': ['Visibility', "distance"],
}
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): "yweather",
vol.Optional("woeid"): vol.Coerce(str),
vol.Optional("forecast"): vol.Coerce(int),
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
[vol.In(SENSOR_TYPES.keys())],
})
# Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Yahoo! weather sensor."""
from yahooweather import get_woeid, UNIT_C, UNIT_F
unit = hass.config.temperature_unit
woeid = config.get("woeid", None)
forecast = config.get("forecast", 0)
# convert unit
yunit = UNIT_C if unit == TEMP_CELSIUS else UNIT_F
# for print HA style temp
SENSOR_TYPES["temperature"][1] = unit
SENSOR_TYPES["temp_min"][1] = unit
SENSOR_TYPES["temp_max"][1] = unit
# if not exists a customer woeid / calc from HA
if woeid is None:
woeid = get_woeid(hass.config.latitude, hass.config.longitude)
# receive a error?
if woeid is None:
_LOGGER.critical("Can't retrieve WOEID from yahoo!")
return False
# create api object
yahoo_api = YahooWeatherData(woeid, yunit)
# if update is false, it will never work...
if not yahoo_api.update():
_LOGGER.critical("Can't retrieve weather data from yahoo!")
return False
# check if forecast support by API
if forecast >= len(yahoo_api.yahoo.Forecast):
_LOGGER.error("Yahoo! only support %d days forcast!",
len(yahoo_api.yahoo.Forecast))
return False
dev = []
for variable in config[CONF_MONITORED_CONDITIONS]:
dev.append(YahooWeatherSensor(yahoo_api, forecast, variable))
add_devices(dev)
# pylint: disable=too-many-instance-attributes
class YahooWeatherSensor(Entity):
"""Implementation of an Yahoo! weather sensor."""
def __init__(self, weather_data, forecast, sensor_type):
"""Initialize the sensor."""
self._client = 'Weather'
self._name = SENSOR_TYPES[sensor_type][0]
self._type = sensor_type
self._state = STATE_UNKNOWN
self._unit = SENSOR_TYPES[sensor_type][1]
self._data = weather_data
self._forecast = forecast
self._code = None
# update data
self.update()
@property
def name(self):
"""Return the name of the sensor."""
return '{} {}'.format(self._client, self._name)
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._data.yahoo.Units.get(self._unit, self._unit)
@property
def entity_picture(self):
"""Return the entity picture to use in the frontend, if any."""
if self._code is None or "weather" not in self._type:
return None
return self._data.yahoo.getWeatherImage(self._code)
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'about': "Weather forecast delivered by Yahoo! Inc. are provided"
" free of charge for use by individuals and non-profit"
" organizations for personal, non-commercial uses."
}
def update(self):
"""Get the latest data from Yahoo! and updates the states."""
self._data.update()
# default code for weather image
self._code = self._data.yahoo.Now["code"]
# read data
if self._type == "weather_current":
self._state = self._data.yahoo.Now["text"]
elif self._type == "weather":
self._code = self._data.yahoo.Forecast[self._forecast]["code"]
self._state = self._data.yahoo.Forecast[self._forecast]["text"]
elif self._type == "temperature":
self._state = self._data.yahoo.Now["temp"]
elif self._type == "temp_min":
self._code = self._data.yahoo.Forecast[self._forecast]["code"]
self._state = self._data.yahoo.Forecast[self._forecast]["low"]
elif self._type == "temp_max":
self._code = self._data.yahoo.Forecast[self._forecast]["code"]
self._state = self._data.yahoo.Forecast[self._forecast]["high"]
elif self._type == "wind_speed":
self._state = self._data.yahoo.Wind["speed"]
elif self._type == "humidity":
self._state = self._data.yahoo.Atmosphere["humidity"]
elif self._type == "pressure":
self._state = self._data.yahoo.Atmosphere["pressure"]
elif self._type == "visibility":
self._state = self._data.yahoo.Atmosphere["visibility"]
# pylint: disable=too-few-public-methods
class YahooWeatherData(object):
"""Handle yahoo api object and limit updates."""
def __init__(self, woeid, temp_unit):
"""Initialize the data object."""
from yahooweather import YahooWeather
# init yahoo api object
self._yahoo = YahooWeather(woeid, temp_unit)
@property
def yahoo(self):
"""Return yahoo api object."""
return self._yahoo
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from yahoo. True is success."""
return self._yahoo.updateWeather()

View File

@ -66,7 +66,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
value.type == zwave.TYPE_DECIMAL):
add_devices([ZWaveMultilevelSensor(value)])
elif value.command_class == zwave.COMMAND_CLASS_ALARM:
elif (value.command_class == zwave.COMMAND_CLASS_ALARM or
value.command_class == zwave.COMMAND_CLASS_SENSOR_ALARM):
add_devices([ZWaveAlarmSensor(value)])

View File

@ -0,0 +1,33 @@
persistent_notification:
create:
description: Show a notification in the frontend
fields:
message:
description: Message body of the notification. [Templates accepted]
example: Please check your configuration.yaml.
title:
description: Optional title for your notification. [Optional, Templates accepted]
example: Test notification
notification_id:
description: Target ID of the notification, will replace a notification with the same Id. [Optional]
example: 1234
homematic:
virtualkey:
description: Press a virtual key from CCU/Homegear or simulate keypress
fields:
address:
description: Address of homematic device or BidCoS-RF for virtual remote
example: BidCoS-RF
channel:
description: Channel for calling a keypress
example: 1
param:
description: Event to send i.e. PRESS_LONG, PRESS_SHORT
example: PRESS_LONG

View File

@ -20,17 +20,17 @@ DEFAULT_PORT = 8125
DEFAULT_PREFIX = 'hass'
DEFAULT_RATE = 1
REQUIREMENTS = ['python-statsd==1.7.2']
REQUIREMENTS = ['statsd==3.2.1']
CONF_HOST = 'host'
CONF_PORT = 'port'
CONF_PREFIX = 'prefix'
CONF_RATE = 'rate'
CONF_ATTR = 'log_attributes'
def setup(hass, config):
"""Setup the StatsD component."""
from statsd.compat import NUM_TYPES
import statsd
conf = config[DOMAIN]
@ -39,16 +39,14 @@ def setup(hass, config):
port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT)
sample_rate = util.convert(conf.get(CONF_RATE), int, DEFAULT_RATE)
prefix = util.convert(conf.get(CONF_PREFIX), str, DEFAULT_PREFIX)
show_attribute_flag = conf.get(CONF_ATTR, False)
statsd_connection = statsd.Connection(
statsd_client = statsd.StatsClient(
host=host,
port=port,
sample_rate=sample_rate,
disabled=False
prefix=prefix
)
meter = statsd.Gauge(prefix, statsd_connection)
def statsd_event_listener(event):
"""Listen for new messages on the bus and sends them to StatsD."""
state = event.data.get('new_state')
@ -61,11 +59,28 @@ def setup(hass, config):
except ValueError:
return
if not isinstance(_state, NUM_TYPES):
return
states = dict(state.attributes)
_LOGGER.debug('Sending %s.%s', state.entity_id, _state)
meter.send(state.entity_id, _state)
if show_attribute_flag is True:
statsd_client.gauge(
"%s.state" % state.entity_id,
_state,
sample_rate
)
# Send attribute values
for key, value in states.items():
if isinstance(value, (float, int)):
stat = "%s.%s" % (state.entity_id, key.replace(' ', '_'))
statsd_client.gauge(stat, value, sample_rate)
else:
statsd_client.gauge(state.entity_id, _state, sample_rate)
# Increment the count
statsd_client.incr(state.entity_id, rate=sample_rate)
hass.bus.listen(EVENT_STATE_CHANGED, statsd_event_listener)

View File

@ -133,7 +133,7 @@ class AcerSwitch(SwitchDevice):
else:
self._available = False
for key in self._attributes.keys():
for key in self._attributes:
msg = CMD_DICT.get(key, None)
if msg:
awns = self._write_read_format(msg)

View File

@ -18,10 +18,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class DemoSwitch(SwitchDevice):
"""represenation of a demo switch."""
"""Representation of a demo switch."""
def __init__(self, name, state, icon, assumed):
"""Initialize the Deom switch."""
"""Initialize the Demo switch."""
self._name = name or DEVICE_DEFAULT_NAME
self._state = state
self._icon = icon

View File

@ -4,7 +4,7 @@ Flux for Home-Assistant.
The idea was taken from https://github.com/KpaBap/hue-flux/
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/switch/flux/
https://home-assistant.io/components/switch.flux/
"""
from datetime import time
import logging
@ -62,7 +62,7 @@ def set_lights_xy(hass, lights, x_val, y_val, brightness):
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the demo switches."""
"""Setup the Flux switches."""
name = config.get(CONF_NAME)
lights = config.get(CONF_LIGHTS)
start_time = config.get(CONF_START_TIME)
@ -85,7 +85,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
# pylint: disable=too-many-instance-attributes
class FluxSwitch(SwitchDevice):
"""Flux switch."""
"""Representation of a Flux switch."""
# pylint: disable=too-many-arguments
def __init__(self, name, hass, state, lights, start_time, stop_time,

View File

@ -0,0 +1,42 @@
"""
Support KNX switching actuators.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.knx/
"""
from homeassistant.components.switch import SwitchDevice
from homeassistant.components.knx import (
KNXConfig, KNXGroupAddress)
DEPENDENCIES = ["knx"]
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Setup the KNX switch platform."""
add_entities([
KNXSwitch(hass, KNXConfig(config))
])
class KNXSwitch(KNXGroupAddress, SwitchDevice):
"""Representation of a KNX switch device."""
def turn_on(self, **kwargs):
"""Turn the switch on.
This sends a value 0 to the group address of the device
"""
self.group_write(1)
self._state = [1]
if not self.should_poll:
self.update_ha_state()
def turn_off(self, **kwargs):
"""Turn the switch off.
This sends a value 1 to the group address of the device
"""
self.group_write(0)
self._state = [0]
if not self.should_poll:
self.update_ha_state()

View File

@ -6,36 +6,38 @@ https://home-assistant.io/components/switch.mystrom/
"""
import logging
import requests
import voluptuous as vol
from homeassistant.const import (CONF_PLATFORM, CONF_NAME, CONF_HOST)
import homeassistant.helpers.config_validation as cv
from homeassistant.components.switch import SwitchDevice
REQUIREMENTS = ['python-mystrom==0.3.6']
DEFAULT_NAME = 'myStrom Switch'
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'mystrom',
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return myStrom switch."""
host = config.get('host')
from pymystrom import MyStromPlug, exceptions
if host is None:
_LOGGER.error('Missing required variable: host')
return False
resource = 'http://{}'.format(host)
host = config.get(CONF_HOST)
try:
requests.get(resource, timeout=10)
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to device %s. "
"Please check the IP address in the configuration file",
host)
MyStromPlug(host).get_status()
except exceptions.MyStromConnectionError:
_LOGGER.error("No route to device '%s'", host)
return False
add_devices([MyStromSwitch(
config.get('name', DEFAULT_NAME),
resource)])
add_devices([MyStromSwitch(config.get('name', DEFAULT_NAME), host)])
class MyStromSwitch(SwitchDevice):
@ -43,10 +45,13 @@ class MyStromSwitch(SwitchDevice):
def __init__(self, name, resource):
"""Initialize the myStrom switch."""
self._state = False
from pymystrom import MyStromPlug
self._name = name
self._resource = resource
self.consumption = 0
self.data = {}
self.plug = MyStromPlug(self._resource)
self.update()
@property
def name(self):
@ -56,45 +61,37 @@ class MyStromSwitch(SwitchDevice):
@property
def is_on(self):
"""Return true if switch is on."""
return self._state
return bool(self.data['relay'])
@property
def current_power_mwh(self):
"""Return the urrent power consumption in mWh."""
return self.consumption
"""Return the current power consumption in mWh."""
return round(self.data['power'], 2)
def turn_on(self, **kwargs):
"""Turn the switch on."""
from pymystrom import exceptions
try:
request = requests.get('{}/relay'.format(self._resource),
params={'state': '1'},
timeout=10)
if request.status_code == 200:
self._state = True
except requests.exceptions.ConnectionError:
_LOGGER.error("Can't turn on %s. Is device offline?",
self.plug.set_relay_on()
except exceptions.MyStromConnectionError:
_LOGGER.error("No route to device '%s'. Is device offline?",
self._resource)
def turn_off(self, **kwargs):
"""Turn the switch off."""
from pymystrom import exceptions
try:
request = requests.get('{}/relay'.format(self._resource),
params={'state': '0'},
timeout=10)
if request.status_code == 200:
self._state = False
except requests.exceptions.ConnectionError:
_LOGGER.error("Can't turn on %s. Is device offline?",
self.plug.set_relay_off()
except exceptions.MyStromConnectionError:
_LOGGER.error("No route to device '%s'. Is device offline?",
self._resource)
def update(self):
"""Get the latest data from REST API and update the state."""
"""Get the latest data from the device and update the data."""
from pymystrom import exceptions
try:
request = requests.get('{}/report'.format(self._resource),
timeout=10)
data = request.json()
self._state = bool(data['relay'])
self.consumption = data['power']
except requests.exceptions.ConnectionError:
self.data = self.plug.get_status()
except exceptions.MyStromConnectionError:
self.data = {'power': 0, 'relay': False}
_LOGGER.error("No route to device '%s'. Is device offline?",
self._resource)

View File

@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = []
for device in devices:
for type_name in SWITCH_TYPES.keys():
for type_name in SWITCH_TYPES:
dev.append(ThinkingCleanerSwitch(device, type_name,
update_devices))

View File

@ -0,0 +1,52 @@
"""
Support for TPLink HS100/HS110 smart switch.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.tplink/
"""
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import (
CONF_HOST, CONF_NAME)
# constants
DEVICE_DEFAULT_NAME = 'HS100'
REQUIREMENTS = ['https://github.com/gadgetreactor/pyHS100/archive/'
'master.zip#pyHS100==0.1.2']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the TPLink switch platform."""
from pyHS100.pyHS100 import SmartPlug
host = config.get(CONF_HOST)
name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME)
add_devices_callback([SmartPlugSwitch(SmartPlug(host),
name)])
class SmartPlugSwitch(SwitchDevice):
"""Representation of a TPLink Smart Plug switch."""
def __init__(self, smartplug, name):
"""Initialize the switch."""
self.smartplug = smartplug
self._name = name
@property
def name(self):
"""Return the name of the Smart Plug, if any."""
return self._name
@property
def is_on(self):
"""Return true if switch is on."""
return self.smartplug.state == 'ON'
def turn_on(self, **kwargs):
"""Turn the switch on."""
self.smartplug.state = 'ON'
def turn_off(self):
"""Turn the switch off."""
self.smartplug.state = 'OFF'

View File

@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Verisure platform."""
"""Setup the Verisure switch platform."""
if not int(hub.config.get('smartplugs', '1')):
return False

View File

@ -10,7 +10,7 @@ from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.entity import ToggleEntity
REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.6']
REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2']
def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -12,7 +12,7 @@ DEPENDENCIES = ["zigbee"]
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Create and add an entity based on the configuration."""
"""Setup the ZigBee switch platform."""
add_entities([
ZigBeeSwitch(hass, ZigBeeDigitalOutConfig(config))
])

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