commit
1f046972d9
171 changed files with 6269 additions and 1899 deletions
16
.coveragerc
16
.coveragerc
|
@ -59,6 +59,9 @@ omit =
|
|||
homeassistant/components/lutron.py
|
||||
homeassistant/components/*/lutron.py
|
||||
|
||||
homeassistant/components/lutron_caseta.py
|
||||
homeassistant/components/*/lutron_caseta.py
|
||||
|
||||
homeassistant/components/modbus.py
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
|
@ -115,9 +118,6 @@ omit =
|
|||
homeassistant/components/zigbee.py
|
||||
homeassistant/components/*/zigbee.py
|
||||
|
||||
homeassistant/components/zwave/*
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
|
@ -145,6 +145,9 @@ omit =
|
|||
homeassistant/components/maxcube.py
|
||||
homeassistant/components/*/maxcube.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
|
@ -171,6 +174,7 @@ omit =
|
|||
homeassistant/components/climate/oem.py
|
||||
homeassistant/components/climate/proliphix.py
|
||||
homeassistant/components/climate/radiotherm.py
|
||||
homeassistant/components/config/zwave.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
homeassistant/components/cover/myq.py
|
||||
|
@ -269,6 +273,7 @@ omit =
|
|||
homeassistant/components/media_player/sonos.py
|
||||
homeassistant/components/media_player/squeezebox.py
|
||||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
|
@ -327,7 +332,6 @@ omit =
|
|||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/emoncms.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
|
@ -352,6 +356,7 @@ omit =
|
|||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/lyft.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/modem_callerid.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
|
@ -429,6 +434,9 @@ omit =
|
|||
homeassistant/components/weather/openweathermap.py
|
||||
homeassistant/components/weather/zamg.py
|
||||
homeassistant/components/zeroconf.py
|
||||
homeassistant/components/zwave/__init__.py
|
||||
homeassistant/components/zwave/util.py
|
||||
homeassistant/components/zwave/workaround.py
|
||||
|
||||
|
||||
[report]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
include README.rst
|
||||
include LICENSE
|
||||
include LICENSE.md
|
||||
graft homeassistant
|
||||
prune homeassistant/components/frontend/www_static/home-assistant-polymer
|
||||
recursive-exclude * *.py[co]
|
||||
|
|
|
@ -255,10 +255,13 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
|
|||
|
||||
def cmdline() -> List[str]:
|
||||
"""Collect path and arguments to re-execute the current hass instance."""
|
||||
if sys.argv[0].endswith('/__main__.py'):
|
||||
if sys.argv[0].endswith(os.path.sep + '__main__.py'):
|
||||
modulepath = os.path.dirname(sys.argv[0])
|
||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon']
|
||||
return [sys.executable] + [arg for arg in sys.argv if
|
||||
arg != '--daemon']
|
||||
else:
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
|
|
|
@ -21,7 +21,6 @@ import homeassistant.loader as loader
|
|||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import event_decorators, service
|
||||
from homeassistant.helpers.signal import async_register_signal_handling
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -127,10 +126,6 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||
|
||||
_LOGGER.info('Home Assistant core initialized')
|
||||
|
||||
# Give event decorators access to HASS
|
||||
event_decorators.HASS = hass
|
||||
service.HASS = hass
|
||||
|
||||
# stage 1
|
||||
for component in components:
|
||||
if component not in FIRST_INIT_COMPONENT:
|
||||
|
|
|
@ -113,7 +113,7 @@ def async_setup(hass, config):
|
|||
if not alarm.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
alarm.async_update_ha_state(True))
|
||||
if hasattr(alarm, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
|
|
@ -27,7 +27,7 @@ from homeassistant.components.camera.mjpeg import (
|
|||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
|
||||
|
||||
DOMAIN = 'android_ip_webcam'
|
||||
REQUIREMENTS = ["pydroid-ipcam==0.4"]
|
||||
REQUIREMENTS = ["pydroid-ipcam==0.6"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
@ -125,11 +125,8 @@ SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
|
|||
|
||||
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
|
||||
|
||||
CONF_AUTO_DISCOVERY = 'auto_discovery'
|
||||
CONF_MOTION_SENSOR = 'motion_sensor'
|
||||
|
||||
DEFAULT_AUTO_DISCOVERY = True
|
||||
DEFAULT_MOTION_SENSOR = False
|
||||
DEFAULT_NAME = 'IP Webcam'
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
@ -145,14 +142,11 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
cv.time_period,
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_AUTO_DISCOVERY, default=DEFAULT_AUTO_DISCOVERY):
|
||||
cv.boolean,
|
||||
vol.Optional(CONF_SWITCHES, default=[]):
|
||||
vol.Optional(CONF_SWITCHES, default=None):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
vol.Optional(CONF_SENSORS, default=[]):
|
||||
vol.Optional(CONF_SENSORS, default=None):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
vol.Optional(CONF_MOTION_SENSOR, default=DEFAULT_MOTION_SENSOR):
|
||||
cv.boolean,
|
||||
vol.Optional(CONF_MOTION_SENSOR, default=None): cv.boolean,
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -184,6 +178,18 @@ def async_setup(hass, config):
|
|||
timeout=cam_config[CONF_TIMEOUT]
|
||||
)
|
||||
|
||||
if switches is None:
|
||||
switches = [setting for setting in cam.enabled_settings
|
||||
if setting in SWITCHES]
|
||||
|
||||
if sensors is None:
|
||||
sensors = [sensor for sensor in cam.enabled_sensors
|
||||
if sensor in SENSORS]
|
||||
sensors.extend(['audio_connections', 'video_connections'])
|
||||
|
||||
if motion is None:
|
||||
motion = 'motion_active' in cam.enabled_sensors
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_data(now):
|
||||
"""Update data from ipcam in SCAN_INTERVAL."""
|
||||
|
@ -195,20 +201,6 @@ def async_setup(hass, config):
|
|||
|
||||
yield from async_update_data(None)
|
||||
|
||||
# use autodiscovery to detect sensors/configs
|
||||
if cam_config[CONF_AUTO_DISCOVERY]:
|
||||
if not cam.available:
|
||||
_LOGGER.error(
|
||||
"Android webcam %s not found for discovery!", cam.base_url)
|
||||
return
|
||||
|
||||
sensors = [sensor for sensor in cam.enabled_sensors
|
||||
if sensor in SENSORS]
|
||||
switches = [setting for setting in cam.enabled_settings
|
||||
if setting in SWITCHES]
|
||||
motion = True if 'motion_active' in cam.enabled_sensors else False
|
||||
sensors.extend(['audio_connections', 'video_connections'])
|
||||
|
||||
# load platforms
|
||||
webcams[host] = cam
|
||||
|
||||
|
|
|
@ -327,6 +327,8 @@ class APIEventForwardingView(HomeAssistantView):
|
|||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Setup an event forwarder."""
|
||||
_LOGGER.warning('Event forwarding is deprecated. '
|
||||
'Will be removed by 0.43')
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
|
|
|
@ -36,6 +36,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
dev = []
|
||||
for droplet in droplets:
|
||||
droplet_id = digital_ocean.DIGITAL_OCEAN.get_droplet_id(droplet)
|
||||
if droplet_id is None:
|
||||
_LOGGER.error("Droplet %s is not available", droplet)
|
||||
return False
|
||||
dev.append(DigitalOceanBinarySensor(
|
||||
digital_ocean.DIGITAL_OCEAN, droplet_id))
|
||||
|
||||
|
|
157
homeassistant/components/binary_sensor/workday.py
Normal file
157
homeassistant/components/binary_sensor/workday.py
Normal file
|
@ -0,0 +1,157 @@
|
|||
"""Sensor to indicate whether the current day is a workday."""
|
||||
import asyncio
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
||||
CONF_NAME, WEEKDAYS)
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['holidays==0.8.1']
|
||||
|
||||
# List of all countries currently supported by holidays
|
||||
# There seems to be no way to get the list out at runtime
|
||||
ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Canada', 'CA',
|
||||
'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', 'England',
|
||||
'EuropeanCentralBank', 'ECB', 'TAR', 'Germany', 'DE',
|
||||
'Ireland', 'Isle of Man', 'Mexico', 'MX', 'Netherlands', 'NL',
|
||||
'NewZealand', 'NZ', 'Northern Ireland', 'Norway', 'NO',
|
||||
'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Spain',
|
||||
'ES', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales']
|
||||
CONF_COUNTRY = 'country'
|
||||
CONF_PROVINCE = 'province'
|
||||
CONF_WORKDAYS = 'workdays'
|
||||
# By default, Monday - Friday are workdays
|
||||
DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri']
|
||||
CONF_EXCLUDES = 'excludes'
|
||||
# By default, public holidays, Saturdays and Sundays are excluded from workdays
|
||||
DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday']
|
||||
DEFAULT_NAME = 'Workday Sensor'
|
||||
ALLOWED_DAYS = WEEKDAYS + ['holiday']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
|
||||
vol.Optional(CONF_PROVINCE, default=None): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
|
||||
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Workday sensor."""
|
||||
import holidays
|
||||
|
||||
# Get the Sensor name from the config
|
||||
sensor_name = config.get(CONF_NAME)
|
||||
|
||||
# Get the country code from the config
|
||||
country = config.get(CONF_COUNTRY)
|
||||
|
||||
# Get the province from the config
|
||||
province = config.get(CONF_PROVINCE)
|
||||
|
||||
# Get the list of workdays from the config
|
||||
workdays = config.get(CONF_WORKDAYS)
|
||||
|
||||
# Get the list of excludes from the config
|
||||
excludes = config.get(CONF_EXCLUDES)
|
||||
|
||||
# Instantiate the holidays module for the current year
|
||||
year = datetime.datetime.now().year
|
||||
obj_holidays = getattr(holidays, country)(years=year)
|
||||
|
||||
# Also apply the provience, if available for the configured country
|
||||
if province:
|
||||
if province not in obj_holidays.PROVINCES:
|
||||
_LOGGER.error('There is no province/state %s in country %s',
|
||||
province, country)
|
||||
return False
|
||||
else:
|
||||
year = datetime.datetime.now().year
|
||||
obj_holidays = getattr(holidays, country)(prov=province,
|
||||
years=year)
|
||||
|
||||
# Output found public holidays via the debug channel
|
||||
_LOGGER.debug("Found the following holidays for your configuration:")
|
||||
for date, name in sorted(obj_holidays.items()):
|
||||
_LOGGER.debug("%s %s", date, name)
|
||||
|
||||
# Add ourselves as device
|
||||
add_devices([IsWorkdaySensor(obj_holidays, workdays,
|
||||
excludes, sensor_name)], True)
|
||||
|
||||
|
||||
def day_to_string(day):
|
||||
"""Convert day index 0 - 7 to string."""
|
||||
try:
|
||||
return ALLOWED_DAYS[day]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
class IsWorkdaySensor(Entity):
|
||||
"""Implementation of a Workday sensor."""
|
||||
|
||||
def __init__(self, obj_holidays, workdays, excludes, name):
|
||||
"""Initialize the Workday sensor."""
|
||||
self._name = name
|
||||
self._obj_holidays = obj_holidays
|
||||
self._workdays = workdays
|
||||
self._excludes = excludes
|
||||
self._state = STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self._state
|
||||
|
||||
def is_include(self, day, now):
|
||||
"""Check if given day is in the includes list."""
|
||||
# Check includes
|
||||
if day in self._workdays:
|
||||
return True
|
||||
elif 'holiday' in self._workdays and now in self._obj_holidays:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_exclude(self, day, now):
|
||||
"""Check if given day is in the excludes list."""
|
||||
# Check excludes
|
||||
if day in self._excludes:
|
||||
return True
|
||||
elif 'holiday' in self._excludes and now in self._obj_holidays:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get date and look whether it is a holiday."""
|
||||
# Default is no workday
|
||||
self._state = STATE_OFF
|
||||
|
||||
# Get iso day of the week (1 = Monday, 7 = Sunday)
|
||||
day = datetime.datetime.today().isoweekday() - 1
|
||||
day_of_week = day_to_string(day)
|
||||
|
||||
if self.is_include(day_of_week, dt_util.now()):
|
||||
self._state = STATE_ON
|
||||
|
||||
if self.is_exclude(day_of_week, dt_util.now()):
|
||||
self._state = STATE_OFF
|
|
@ -19,34 +19,34 @@ _LOGGER = logging.getLogger(__name__)
|
|||
DEPENDENCIES = []
|
||||
|
||||
|
||||
def get_device(value, **kwargs):
|
||||
def get_device(values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
device_mapping = workaround.get_device_mapping(value)
|
||||
device_mapping = workaround.get_device_mapping(values.primary)
|
||||
if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT:
|
||||
# Default the multiplier to 4
|
||||
re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4)
|
||||
return ZWaveTriggerSensor(value, "motion", re_arm_multiplier * 8)
|
||||
re_arm_multiplier = zwave.get_config_value(values.primary.node, 9) or 4
|
||||
return ZWaveTriggerSensor(values, "motion", re_arm_multiplier * 8)
|
||||
|
||||
if workaround.get_device_component_mapping(value) == DOMAIN:
|
||||
return ZWaveBinarySensor(value, None)
|
||||
if workaround.get_device_component_mapping(values.primary) == DOMAIN:
|
||||
return ZWaveBinarySensor(values, None)
|
||||
|
||||
if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||
return ZWaveBinarySensor(value, None)
|
||||
if values.primary.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY:
|
||||
return ZWaveBinarySensor(values, None)
|
||||
return None
|
||||
|
||||
|
||||
class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
||||
"""Representation of a binary sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, device_class):
|
||||
def __init__(self, values, device_class):
|
||||
"""Initialize the sensor."""
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._sensor_type = device_class
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
@ -58,24 +58,19 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity):
|
|||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
|
||||
class ZWaveTriggerSensor(ZWaveBinarySensor):
|
||||
"""Representation of a stateless sensor within Z-Wave."""
|
||||
|
||||
def __init__(self, value, device_class, re_arm_sec=60):
|
||||
def __init__(self, values, device_class, re_arm_sec=60):
|
||||
"""Initialize the sensor."""
|
||||
super(ZWaveTriggerSensor, self).__init__(value, device_class)
|
||||
super(ZWaveTriggerSensor, self).__init__(values, device_class)
|
||||
self.re_arm_sec = re_arm_sec
|
||||
self.invalidate_after = None
|
||||
|
||||
def update_properties(self):
|
||||
"""Called when a value for this entity's node has changed."""
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
# only allow this value to be true for re_arm secs
|
||||
if not self.hass:
|
||||
return
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.helpers import discovery
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'blink'
|
||||
REQUIREMENTS = ['blinkpy==0.4.4']
|
||||
REQUIREMENTS = ['blinkpy==0.5.2']
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.util import Throttle
|
|||
|
||||
DEPENDENCIES = ['blink']
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
"""
|
||||
Support for internal dispatcher image push to Camera.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera.dispatcher/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_SIGNAL = 'signal'
|
||||
DEFAULT_NAME = 'Dispatcher Camera'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SIGNAL): cv.slugify,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a dispatcher camera."""
|
||||
if discovery_info:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
async_add_devices(
|
||||
[DispatcherCamera(config[CONF_NAME], config[CONF_SIGNAL])])
|
||||
|
||||
|
||||
class DispatcherCamera(Camera):
|
||||
"""A dispatcher implementation of an camera."""
|
||||
|
||||
def __init__(self, name, signal):
|
||||
"""Initialize a dispatcher camera."""
|
||||
super().__init__()
|
||||
self._name = name
|
||||
self._signal = signal
|
||||
self._image = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register dispatcher and callbacks."""
|
||||
@callback
|
||||
def async_update_image(image):
|
||||
"""Update image from dispatcher call."""
|
||||
self._image = image
|
||||
|
||||
async_dispatcher_connect(self.hass, self._signal, async_update_image)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
return self._image
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this device."""
|
||||
return self._name
|
|
@ -14,7 +14,7 @@ import async_timeout
|
|||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
|
||||
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, CONF_TIMEOUT)
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
|
@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
DEFAULT_NAME = 'Synology Camera'
|
||||
DEFAULT_STREAM_ID = '0'
|
||||
TIMEOUT = 5
|
||||
DEFAULT_TIMEOUT = 5
|
||||
CONF_CAMERA_NAME = 'camera_name'
|
||||
CONF_STREAM_ID = 'stream_id'
|
||||
|
||||
|
@ -51,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||
})
|
||||
|
@ -60,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a Synology IP Camera."""
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
websession_init = async_get_clientsession(hass, verify_ssl)
|
||||
|
||||
# Determine API to use for authentication
|
||||
|
@ -74,7 +76,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
}
|
||||
query_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
query_req = yield from websession_init.get(
|
||||
syno_api_url,
|
||||
params=query_payload
|
||||
|
@ -103,7 +105,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
websession_init,
|
||||
config.get(CONF_USERNAME),
|
||||
config.get(CONF_PASSWORD),
|
||||
syno_auth_url
|
||||
syno_auth_url,
|
||||
timeout
|
||||
)
|
||||
|
||||
# init websession
|
||||
|
@ -120,7 +123,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
'version': '1'
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
camera_req = yield from websession.get(
|
||||
syno_camera_url,
|
||||
params=camera_payload
|
||||
|
@ -149,7 +152,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
snapshot_path,
|
||||
streaming_path,
|
||||
camera_path,
|
||||
auth_path
|
||||
auth_path,
|
||||
timeout
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
|
@ -157,7 +161,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def get_session_id(hass, websession, username, password, login_url):
|
||||
def get_session_id(hass, websession, username, password, login_url, timeout):
|
||||
"""Get a session id."""
|
||||
auth_payload = {
|
||||
'api': AUTH_API,
|
||||
|
@ -170,7 +174,7 @@ def get_session_id(hass, websession, username, password, login_url):
|
|||
}
|
||||
auth_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
auth_req = yield from websession.get(
|
||||
login_url,
|
||||
params=auth_payload
|
||||
|
@ -192,7 +196,7 @@ class SynologyCamera(Camera):
|
|||
|
||||
def __init__(self, hass, websession, config, camera_id,
|
||||
camera_name, snapshot_path, streaming_path, camera_path,
|
||||
auth_path):
|
||||
auth_path, timeout):
|
||||
"""Initialize a Synology Surveillance Station camera."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
|
@ -206,6 +210,7 @@ class SynologyCamera(Camera):
|
|||
self._streaming_path = streaming_path
|
||||
self._camera_path = camera_path
|
||||
self._auth_path = auth_path
|
||||
self._timeout = timeout
|
||||
|
||||
def camera_image(self):
|
||||
"""Return bytes of camera image."""
|
||||
|
@ -225,7 +230,7 @@ class SynologyCamera(Camera):
|
|||
'cameraId': self._camera_id
|
||||
}
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
||||
response = yield from self._websession.get(
|
||||
image_url,
|
||||
params=image_payload
|
||||
|
|
|
@ -19,6 +19,9 @@ _LOGGER = logging.getLogger(__name__)
|
|||
DEPENDENCIES = ['zoneminder']
|
||||
DOMAIN = 'zoneminder'
|
||||
|
||||
# From ZoneMinder's web/includes/config.php.in
|
||||
ZM_STATE_ALARM = "2"
|
||||
|
||||
|
||||
def _get_image_url(hass, monitor, mode):
|
||||
zm_data = hass.data[DOMAIN]
|
||||
|
@ -69,10 +72,43 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
CONF_MJPEG_URL: _get_image_url(hass, monitor, 'jpeg'),
|
||||
CONF_STILL_IMAGE_URL: _get_image_url(hass, monitor, 'single')
|
||||
}
|
||||
cameras.append(MjpegCamera(hass, device_info))
|
||||
cameras.append(ZoneMinderCamera(hass, device_info, monitor))
|
||||
|
||||
if not cameras:
|
||||
_LOGGER.warning('No active cameras found')
|
||||
return
|
||||
|
||||
async_add_devices(cameras)
|
||||
|
||||
|
||||
class ZoneMinderCamera(MjpegCamera):
|
||||
"""Representation of a ZoneMinder Monitor Stream."""
|
||||
|
||||
def __init__(self, hass, device_info, monitor):
|
||||
"""Initialize as a subclass of MjpegCamera."""
|
||||
super().__init__(hass, device_info)
|
||||
self._monitor_id = int(monitor['Id'])
|
||||
self._is_recording = None
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Update the recording state periodically."""
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
"""Update our recording state from the ZM API."""
|
||||
_LOGGER.debug('Updating camera state for monitor %i', self._monitor_id)
|
||||
status_response = zoneminder.get_state(
|
||||
'api/monitors/alarm/id:%i/command:status.json' % self._monitor_id
|
||||
)
|
||||
if not status_response:
|
||||
_LOGGER.warning('Could not get status for monitor %i',
|
||||
self._monitor_id)
|
||||
return
|
||||
|
||||
self._is_recording = status_response['status'] == ZM_STATE_ALARM
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return whether the monitor is in alarm mode."""
|
||||
return self._is_recording
|
||||
|
|
|
@ -224,7 +224,7 @@ def async_setup(hass, config):
|
|||
if not climate.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
climate.async_update_ha_state(True))
|
||||
if hasattr(climate, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
|
296
homeassistant/components/climate/tado.py
Normal file
296
homeassistant/components/climate/tado.py
Normal file
|
@ -0,0 +1,296 @@
|
|||
"""tado component to create a climate device for each zone."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice)
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE)
|
||||
from homeassistant.components.tado import (
|
||||
DATA_TADO)
|
||||
|
||||
CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Default mytado mode
|
||||
CONST_MODE_OFF = "OFF" # Switch off heating in a zone
|
||||
|
||||
# When we change the temperature setting, we need an overlay mode
|
||||
# wait until tado changes the mode automatic
|
||||
CONST_OVERLAY_TADO_MODE = "TADO_MODE"
|
||||
# the user has change the temperature or mode manually
|
||||
CONST_OVERLAY_MANUAL = "MANUAL"
|
||||
# the temperature will be reset after a timespan
|
||||
CONST_OVERLAY_TIMER = "TIMER"
|
||||
|
||||
OPERATION_LIST = {
|
||||
CONST_OVERLAY_MANUAL: "Manual",
|
||||
CONST_OVERLAY_TIMER: "Timer",
|
||||
CONST_OVERLAY_TADO_MODE: "Tado mode",
|
||||
CONST_MODE_SMART_SCHEDULE: "Smart schedule",
|
||||
CONST_MODE_OFF: "Off"}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the climate platform."""
|
||||
# get the PyTado object from the hub component
|
||||
tado = hass.data[DATA_TADO]
|
||||
|
||||
try:
|
||||
zones = tado.get_zones()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to get zone info from mytado")
|
||||
return False
|
||||
|
||||
climate_devices = []
|
||||
for zone in zones:
|
||||
climate_devices.append(create_climate_device(tado, hass,
|
||||
zone,
|
||||
zone['name'],
|
||||
zone['id']))
|
||||
|
||||
if len(climate_devices) > 0:
|
||||
add_devices(climate_devices, True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def create_climate_device(tado, hass, zone, name, zone_id):
|
||||
"""Create a climate device."""
|
||||
capabilities = tado.get_capabilities(zone_id)
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
min_temp = float(capabilities["temperatures"]["celsius"]["min"])
|
||||
max_temp = float(capabilities["temperatures"]["celsius"]["max"])
|
||||
ac_mode = capabilities["type"] != "HEATING"
|
||||
|
||||
data_id = 'zone {} {}'.format(name, zone_id)
|
||||
device = TadoClimate(tado,
|
||||
name, zone_id, data_id,
|
||||
hass.config.units.temperature(min_temp, unit),
|
||||
hass.config.units.temperature(max_temp, unit),
|
||||
ac_mode)
|
||||
|
||||
tado.add_sensor(data_id, {
|
||||
"id": zone_id,
|
||||
"zone": zone,
|
||||
"name": name,
|
||||
"climate": device
|
||||
})
|
||||
|
||||
return device
|
||||
|
||||
|
||||
class TadoClimate(ClimateDevice):
|
||||
"""Representation of a tado climate device."""
|
||||
|
||||
def __init__(self, store, zone_name, zone_id, data_id,
|
||||
min_temp, max_temp, ac_mode,
|
||||
tolerance=0.3):
|
||||
"""Initialization of TadoClimate device."""
|
||||
self._store = store
|
||||
self._data_id = data_id
|
||||
|
||||
self.zone_name = zone_name
|
||||
self.zone_id = zone_id
|
||||
|
||||
self.ac_mode = ac_mode
|
||||
|
||||
self._active = False
|
||||
self._device_is_active = False
|
||||
|
||||
self._unit = TEMP_CELSIUS
|
||||
self._cur_temp = None
|
||||
self._cur_humidity = None
|
||||
self._is_away = False
|
||||
self._min_temp = min_temp
|
||||
self._max_temp = max_temp
|
||||
self._target_temp = None
|
||||
self._tolerance = tolerance
|
||||
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self.zone_name
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
return self._cur_humidity
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the sensor temperature."""
|
||||
return self._cur_temp
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current readable operation mode."""
|
||||
return OPERATION_LIST.get(self._current_operation)
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes (readable)."""
|
||||
return list(OPERATION_LIST.values())
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""The unit of measurement used by the platform."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
return self._is_away
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
self._current_operation = CONST_OVERLAY_TADO_MODE
|
||||
self._overlay_mode = None
|
||||
self._target_temp = temperature
|
||||
self._control_heating()
|
||||
|
||||
def set_operation_mode(self, readable_operation_mode):
|
||||
"""Set new operation mode."""
|
||||
operation_mode = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
for mode, readable in OPERATION_LIST.items():
|
||||
if readable == readable_operation_mode:
|
||||
operation_mode = mode
|
||||
break
|
||||
|
||||
self._current_operation = operation_mode
|
||||
self._overlay_mode = None
|
||||
self._control_heating()
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
if self._min_temp:
|
||||
return self._min_temp
|
||||
else:
|
||||
# get default temp from super class
|
||||
return super().min_temp
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
if self._max_temp:
|
||||
return self._max_temp
|
||||
else:
|
||||
# Get default temp from super class
|
||||
return super().max_temp
|
||||
|
||||
def update(self):
|
||||
"""Update the state of this climate device."""
|
||||
self._store.update()
|
||||
|
||||
data = self._store.get_data(self._data_id)
|
||||
|
||||
if data is None:
|
||||
_LOGGER.debug('Recieved no data for zone %s',
|
||||
self.zone_name)
|
||||
return
|
||||
|
||||
if 'sensorDataPoints' in data:
|
||||
sensor_data = data['sensorDataPoints']
|
||||
temperature = float(
|
||||
sensor_data['insideTemperature']['celsius'])
|
||||
humidity = float(
|
||||
sensor_data['humidity']['percentage'])
|
||||
setting = 0
|
||||
|
||||
# temperature setting will not exist when device is off
|
||||
if 'temperature' in data['setting'] and \
|
||||
data['setting']['temperature'] is not None:
|
||||
setting = float(
|
||||
data['setting']['temperature']['celsius'])
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
|
||||
self._cur_temp = self.hass.config.units.temperature(
|
||||
temperature, unit)
|
||||
|
||||
self._target_temp = self.hass.config.units.temperature(
|
||||
setting, unit)
|
||||
|
||||
self._cur_humidity = humidity
|
||||
|
||||
if 'tadoMode' in data:
|
||||
mode = data['tadoMode']
|
||||
self._is_away = mode == "AWAY"
|
||||
|
||||
if 'setting' in data:
|
||||
power = data['setting']['power']
|
||||
if power == "OFF":
|
||||
self._current_operation = CONST_MODE_OFF
|
||||
self._device_is_active = False
|
||||
else:
|
||||
self._device_is_active = True
|
||||
|
||||
if 'overlay' in data and data['overlay'] is not None:
|
||||
overlay = True
|
||||
termination = data['overlay']['termination']['type']
|
||||
else:
|
||||
overlay = False
|
||||
termination = ""
|
||||
|
||||
# if you set mode manualy to off, there will be an overlay
|
||||
# and a termination, but we want to see the mode "OFF"
|
||||
|
||||
if overlay and self._device_is_active:
|
||||
# there is an overlay the device is on
|
||||
self._overlay_mode = termination
|
||||
self._current_operation = termination
|
||||
else:
|
||||
# there is no overlay, the mode will always be
|
||||
# "SMART_SCHEDULE"
|
||||
self._overlay_mode = CONST_MODE_SMART_SCHEDULE
|
||||
self._current_operation = CONST_MODE_SMART_SCHEDULE
|
||||
|
||||
def _control_heating(self):
|
||||
"""Send new target temperature to mytado."""
|
||||
if not self._active and None not in (self._cur_temp,
|
||||
self._target_temp):
|
||||
self._active = True
|
||||
_LOGGER.info('Obtained current and target temperature. '
|
||||
'tado thermostat active.')
|
||||
|
||||
if not self._active or self._current_operation == self._overlay_mode:
|
||||
return
|
||||
|
||||
if self._current_operation == CONST_MODE_SMART_SCHEDULE:
|
||||
_LOGGER.info('Switching mytado.com to SCHEDULE (default) '
|
||||
'for zone %s', self.zone_name)
|
||||
self._store.reset_zone_overlay(self.zone_id)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
if self._current_operation == CONST_MODE_OFF:
|
||||
_LOGGER.info('Switching mytado.com to OFF for zone %s',
|
||||
self.zone_name)
|
||||
self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL)
|
||||
self._overlay_mode = self._current_operation
|
||||
return
|
||||
|
||||
_LOGGER.info('Switching mytado.com to %s mode for zone %s',
|
||||
self._current_operation, self.zone_name)
|
||||
self._store.set_zone_overlay(self.zone_id,
|
||||
self._current_operation,
|
||||
self._target_temp)
|
||||
|
||||
self._overlay_mode = self._current_operation
|
|
@ -85,11 +85,11 @@ class VeraThermostat(VeraDevice, ClimateDevice):
|
|||
return self.vera_device.fan_cycle()
|
||||
|
||||
@property
|
||||
def current_power_mwh(self):
|
||||
"""Current power usage in mWh."""
|
||||
def current_power_w(self):
|
||||
"""Current power usage in W."""
|
||||
power = self.vera_device.power
|
||||
if power:
|
||||
return convert(power, float, 0.0) * 1000
|
||||
return convert(power, float, 0.0)
|
||||
|
||||
def update(self):
|
||||
"""Called by the vera device callback to update state."""
|
||||
|
|
|
@ -10,7 +10,6 @@ import logging
|
|||
from homeassistant.components.climate import DOMAIN
|
||||
from homeassistant.components.climate import ClimateDevice
|
||||
from homeassistant.components.zwave import ZWaveDeviceEntity
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
|
@ -33,20 +32,18 @@ DEVICE_MAPPINGS = {
|
|||
}
|
||||
|
||||
|
||||
def get_device(hass, value, **kwargs):
|
||||
def get_device(hass, values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
return ZWaveClimate(value, temp_unit)
|
||||
return ZWaveClimate(values, temp_unit)
|
||||
|
||||
|
||||
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
||||
"""Representation of a Z-Wave Climate device."""
|
||||
|
||||
def __init__(self, value, temp_unit):
|
||||
def __init__(self, values, temp_unit):
|
||||
"""Initialize the Z-Wave climate device."""
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._index = value.index
|
||||
self._node = value.node
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._target_temperature = None
|
||||
self._current_temperature = None
|
||||
self._current_operation = None
|
||||
|
@ -61,10 +58,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
_LOGGER.debug("temp_unit is %s", self._unit)
|
||||
self._zxt_120 = None
|
||||
# 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 (self.node.manufacturer_id.strip() and
|
||||
self.node.product_id.strip()):
|
||||
specific_sensor_key = (
|
||||
int(self.node.manufacturer_id, 16),
|
||||
int(self.node.product_id, 16))
|
||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120:
|
||||
_LOGGER.debug("Remotec ZXT-120 Zwave Thermostat"
|
||||
|
@ -75,86 +73,58 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
# Operation Mode
|
||||
self._current_operation = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE, member='data')
|
||||
operation_list = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
|
||||
member='data_items')
|
||||
if operation_list:
|
||||
self._operation_list = list(operation_list)
|
||||
if self.values.mode:
|
||||
self._current_operation = self.values.mode.data
|
||||
operation_list = self.values.mode.data_items
|
||||
if operation_list:
|
||||
self._operation_list = list(operation_list)
|
||||
_LOGGER.debug("self._operation_list=%s", self._operation_list)
|
||||
_LOGGER.debug("self._current_operation=%s", self._current_operation)
|
||||
|
||||
# Current Temp
|
||||
self._current_temperature = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
label=['Temperature'], member='data')
|
||||
device_unit = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL,
|
||||
label=['Temperature'], member='units')
|
||||
if device_unit is not None:
|
||||
self._unit = device_unit
|
||||
if self.values.temperature:
|
||||
self._current_temperature = self.values.temperature.data
|
||||
device_unit = self.values.temperature.units
|
||||
if device_unit is not None:
|
||||
self._unit = device_unit
|
||||
|
||||
# Fan Mode
|
||||
self._current_fan_mode = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
member='data')
|
||||
fan_list = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
member='data_items')
|
||||
if fan_list:
|
||||
self._fan_list = list(fan_list)
|
||||
if self.values.fan_mode:
|
||||
self._current_fan_mode = self.values.fan_mode.data
|
||||
fan_list = self.values.fan_mode.data_items
|
||||
if fan_list:
|
||||
self._fan_list = list(fan_list)
|
||||
_LOGGER.debug("self._fan_list=%s", self._fan_list)
|
||||
_LOGGER.debug("self._current_fan_mode=%s",
|
||||
self._current_fan_mode)
|
||||
# Swing mode
|
||||
if self._zxt_120 == 1:
|
||||
self._current_swing_mode = (
|
||||
self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33,
|
||||
member='data'))
|
||||
swing_list = self.get_value(class_id=zwave.const
|
||||
.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33,
|
||||
member='data_items')
|
||||
if swing_list:
|
||||
self._swing_list = list(swing_list)
|
||||
if self.values.zxt_120_swing_mode:
|
||||
self._current_swing_mode = self.values.zxt_120_swing_mode.data
|
||||
swing_list = self.values.zxt_120_swing_mode.data_items
|
||||
if swing_list:
|
||||
self._swing_list = list(swing_list)
|
||||
_LOGGER.debug("self._swing_list=%s", self._swing_list)
|
||||
_LOGGER.debug("self._current_swing_mode=%s",
|
||||
self._current_swing_mode)
|
||||
# Set point
|
||||
temps = []
|
||||
for value in (
|
||||
self._node.get_values(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT)
|
||||
.values()):
|
||||
temps.append((round(float(value.data)), 1))
|
||||
if value.index == self._index:
|
||||
if value.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
break
|
||||
else:
|
||||
self._target_temperature = round((float(value.data)), 1)
|
||||
if self.values.primary.data == 0:
|
||||
_LOGGER.debug("Setpoint is 0, setting default to "
|
||||
"current_temperature=%s",
|
||||
self._current_temperature)
|
||||
self._target_temperature = (
|
||||
round((float(self._current_temperature)), 1))
|
||||
else:
|
||||
self._target_temperature = round(
|
||||
(float(self.values.primary.data)), 1)
|
||||
|
||||
# Operating state
|
||||
self._operating_state = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE,
|
||||
member='data')
|
||||
if self.values.operating_state:
|
||||
self._operating_state = self.values.operating_state.data
|
||||
|
||||
# Fan operating state
|
||||
self._fan_state = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE,
|
||||
member='data')
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling on Z-Wave."""
|
||||
return False
|
||||
if self.values.fan_state:
|
||||
self._fan_state = self.values.fan_state.data
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
|
@ -213,41 +183,31 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
|
|||
else:
|
||||
return
|
||||
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT,
|
||||
index=self._index, data=temperature)
|
||||
self.schedule_update_ha_state()
|
||||
self.values.primary.data = temperature
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set new target fan mode."""
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE,
|
||||
index=0, data=bytes(fan, 'utf-8'))
|
||||
if self.values.fan_mode:
|
||||
self.values.fan_mode.data = bytes(fan, 'utf-8')
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new target operation mode."""
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE,
|
||||
index=0, data=bytes(operation_mode, 'utf-8'))
|
||||
if self.values.mode:
|
||||
self.values.mode.data = bytes(operation_mode, 'utf-8')
|
||||
|
||||
def set_swing_mode(self, swing_mode):
|
||||
"""Set new target swing mode."""
|
||||
if self._zxt_120 == 1:
|
||||
self.set_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_CONFIGURATION,
|
||||
index=33, data=bytes(swing_mode, 'utf-8'))
|
||||
if self.values.zxt_120_swing_mode:
|
||||
self.values.zxt_120_swing_mode.data = bytes(
|
||||
swing_mode, 'utf-8')
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
data = super().device_state_attributes
|
||||
if self._operating_state:
|
||||
data[ATTR_OPERATING_STATE] = self._operating_state,
|
||||
data[ATTR_OPERATING_STATE] = self._operating_state
|
||||
if self._fan_state:
|
||||
data[ATTR_FAN_STATE] = self._fan_state
|
||||
return data
|
||||
|
||||
@property
|
||||
def dependent_value_ids(self):
|
||||
"""List of value IDs a device depends on."""
|
||||
return None
|
||||
|
|
|
@ -166,7 +166,7 @@ def async_setup(hass, config):
|
|||
if not cover.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
cover.async_update_ha_state(True))
|
||||
if hasattr(cover, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
|
|
@ -14,8 +14,8 @@ from homeassistant.const import (
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = [
|
||||
'https://github.com/arraylabs/pymyq/archive/v0.0.7.zip'
|
||||
'#pymyq==0.0.7']
|
||||
'https://github.com/arraylabs/pymyq/archive/v0.0.8.zip'
|
||||
'#pymyq==0.0.8']
|
||||
|
||||
COVER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TYPE): cv.string,
|
||||
|
|
|
@ -20,64 +20,49 @@ _LOGGER = logging.getLogger(__name__)
|
|||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
|
||||
def get_device(value, **kwargs):
|
||||
def get_device(values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and value.index == 0):
|
||||
return ZwaveRollershutter(value)
|
||||
elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
|
||||
value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
|
||||
return ZwaveGarageDoor(value)
|
||||
if (values.primary.command_class ==
|
||||
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and values.primary.index == 0):
|
||||
return ZwaveRollershutter(values)
|
||||
elif (values.primary.command_class in [
|
||||
zwave.const.COMMAND_CLASS_SWITCH_BINARY,
|
||||
zwave.const.COMMAND_CLASS_BARRIER_OPERATOR]):
|
||||
return ZwaveGarageDoor(values)
|
||||
return None
|
||||
|
||||
|
||||
class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Representation of an Zwave roller shutter."""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, values):
|
||||
"""Initialize the zwave rollershutter."""
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
# pylint: disable=no-member
|
||||
self._node = value.node
|
||||
self._open_id = None
|
||||
self._close_id = None
|
||||
self._current_position_id = None
|
||||
self._current_position = None
|
||||
|
||||
self._workaround = workaround.get_device_mapping(value)
|
||||
self._workaround = workaround.get_device_mapping(values.primary)
|
||||
if self._workaround:
|
||||
_LOGGER.debug("Using workaround %s", self._workaround)
|
||||
self.update_properties()
|
||||
|
||||
@property
|
||||
def dependent_value_ids(self):
|
||||
"""List of value IDs a device depends on."""
|
||||
if not self._node.is_ready:
|
||||
return None
|
||||
return [self._current_position_id]
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
# Position value
|
||||
if not self._node.is_ready:
|
||||
if self._current_position_id is None:
|
||||
self._current_position_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Level'], member='value_id')
|
||||
if self._open_id is None:
|
||||
self._open_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Open', 'Up'], member='value_id')
|
||||
if self._close_id is None:
|
||||
self._close_id = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
label=['Close', 'Down'], member='value_id')
|
||||
if self._open_id and self._close_id and \
|
||||
self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
|
||||
self._open_id, self._close_id = self._close_id, self._open_id
|
||||
self._workaround = None
|
||||
self._current_position = self._node.get_dimmer_level(
|
||||
self._current_position_id)
|
||||
self._current_position = self.values.primary.data
|
||||
|
||||
if self.values.open and self.values.close and \
|
||||
self._open_id is None and self._close_id is None:
|
||||
if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE:
|
||||
self._open_id = self.values.close.value_id
|
||||
self._close_id = self.values.open.value_id
|
||||
self._workaround = None
|
||||
else:
|
||||
self._open_id = self.values.open.value_id
|
||||
self._close_id = self.values.close.value_id
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
|
@ -112,7 +97,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
self._node.set_dimmer(self._value.value_id, position)
|
||||
self.node.set_dimmer(self.values.primary.value_id, position)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
|
@ -122,14 +107,14 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
||||
"""Representation of an Zwave garage door device."""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, values):
|
||||
"""Initialize the zwave garage door."""
|
||||
ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
|
@ -138,11 +123,11 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, CoverDevice):
|
|||
|
||||
def close_cover(self):
|
||||
"""Close the garage door."""
|
||||
self._value.data = False
|
||||
self.values.primary.data = False
|
||||
|
||||
def open_cover(self):
|
||||
"""Open the garage door."""
|
||||
self._value.data = True
|
||||
self.values.primary.data = True
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import (
|
|||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['fritzconnection==0.6']
|
||||
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
|
|
@ -14,6 +14,7 @@ import requests
|
|||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
|
@ -31,6 +32,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
class InvalidLuciTokenError(HomeAssistantError):
|
||||
"""When an invalid token is detected."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
"""Validate the configuration and return a Luci scanner."""
|
||||
scanner = LuciDeviceScanner(config[DOMAIN])
|
||||
|
@ -46,8 +53,9 @@ class LuciDeviceScanner(DeviceScanner):
|
|||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
host = config[CONF_HOST]
|
||||
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||
self.host = config[CONF_HOST]
|
||||
self.username = config[CONF_USERNAME]
|
||||
self.password = config[CONF_PASSWORD]
|
||||
|
||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||
|
||||
|
@ -55,12 +63,15 @@ class LuciDeviceScanner(DeviceScanner):
|
|||
|
||||
self.last_results = {}
|
||||
|
||||
self.token = _get_token(host, username, password)
|
||||
self.host = host
|
||||
self.refresh_token()
|
||||
|
||||
self.mac2name = None
|
||||
self.success_init = self.token is not None
|
||||
|
||||
def refresh_token(self):
|
||||
"""Get a new token."""
|
||||
self.token = _get_token(self.host, self.username, self.password)
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
@ -98,8 +109,15 @@ class LuciDeviceScanner(DeviceScanner):
|
|||
_LOGGER.info('Checking ARP')
|
||||
|
||||
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
||||
result = _req_json_rpc(url, 'net.arptable',
|
||||
params={'auth': self.token})
|
||||
|
||||
try:
|
||||
result = _req_json_rpc(url, 'net.arptable',
|
||||
params={'auth': self.token})
|
||||
except InvalidLuciTokenError:
|
||||
_LOGGER.info('Refreshing token')
|
||||
self.refresh_token()
|
||||
return False
|
||||
|
||||
if result:
|
||||
self.last_results = []
|
||||
for device_entry in result:
|
||||
|
@ -116,6 +134,7 @@ class LuciDeviceScanner(DeviceScanner):
|
|||
def _req_json_rpc(url, method, *args, **kwargs):
|
||||
"""Perform one JSON RPC operation."""
|
||||
data = json.dumps({'method': method, 'params': args})
|
||||
|
||||
try:
|
||||
res = requests.post(url, data=data, timeout=5, **kwargs)
|
||||
except requests.exceptions.Timeout:
|
||||
|
@ -139,6 +158,10 @@ def _req_json_rpc(url, method, *args, **kwargs):
|
|||
"Failed to authenticate, "
|
||||
"please check your username and password")
|
||||
return
|
||||
elif res.status_code == 403:
|
||||
_LOGGER.error('Luci responded with a 403 Invalid token')
|
||||
raise InvalidLuciTokenError
|
||||
|
||||
else:
|
||||
_LOGGER.error('Invalid response from luci: %s', res)
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
|
|||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-digitalocean==1.10.1']
|
||||
REQUIREMENTS = ['python-digitalocean==1.11']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -218,7 +218,7 @@ def async_setup(hass, config: dict):
|
|||
if not fan.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
fan.async_update_ha_state(True))
|
||||
if hasattr(fan, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
window.Polymer = {
|
||||
lazyRegister: true,
|
||||
useNativeCSSProperties: true,
|
||||
dom: 'shady',
|
||||
dom: 'shadow',
|
||||
suppressTemplateNotifications: true,
|
||||
suppressBindingNotifications: true,
|
||||
};
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
|
||||
FINGERPRINTS = {
|
||||
"compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0",
|
||||
"core.js": "1f7f88d8f5dada08bce1d935cfa5f33e",
|
||||
"frontend.html": "418f6ef8354ce71f1b9594ee2068ebef",
|
||||
"mdi.html": "65413cdf82f822bd6480e577852f0292",
|
||||
"core.js": "5d08475f03adb5969bd31855d5ca0cfd",
|
||||
"frontend.html": "53c45b837a3bcae7cfb9ef4a5919844f",
|
||||
"mdi.html": "4921d26f29dc148c3e8bd5bcd8ce5822",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-config.html": "412b3e24515ffa1ee8074ce974cf4057",
|
||||
"panels/ha-panel-dev-event.html": "91347dedf3b4fa9b49ccf4c0a28a03c4",
|
||||
"panels/ha-panel-config.html": "6dcb246cd356307a638f81c4f89bf9b3",
|
||||
"panels/ha-panel-dev-event.html": "1f169700c2345785855b1d7919d12326",
|
||||
"panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d",
|
||||
"panels/ha-panel-dev-service.html": "153aad076f98bbd626466bac50986874",
|
||||
"panels/ha-panel-dev-state.html": "90f3bede9602241552ef7bb7958198c6",
|
||||
"panels/ha-panel-dev-template.html": "c249a4fc18a3a6994de3d6330cfe6cbb",
|
||||
"panels/ha-panel-history.html": "fdaa4d2402d49d4c8bd64a1708ab7a50",
|
||||
"panels/ha-panel-dev-service.html": "0fe8e6acdccf2dc3d1ae657b2c7f2df0",
|
||||
"panels/ha-panel-dev-state.html": "48d37db4a1d6708314ded1d624d0f4d4",
|
||||
"panels/ha-panel-dev-template.html": "6f353392d68574fbc5af188bca44d0ae",
|
||||
"panels/ha-panel-history.html": "bfd5f929d5aa9cefdd799ec37624efa1",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "2af1feb30b37427f481d5437a438a3f2",
|
||||
"panels/ha-panel-map.html": "e10704a3469e44d1714eac9ed8e4b6a0",
|
||||
"panels/ha-panel-logbook.html": "a1fc2b5d739bedb9d87e4da4cd929a71",
|
||||
"panels/ha-panel-map.html": "9aa065b1908089f3bb5af7fdf9485be5",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
|||
Subproject commit de1b20b70a16aeb7c48a1b4867c97864c88adb1c
|
||||
Subproject commit f4c59e1eff3223262c198a29cf70c62572de019b
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
@ -85,7 +85,7 @@ def setup(hass, config):
|
|||
|
||||
try:
|
||||
influx = InfluxDBClient(**kwargs)
|
||||
influx.query("SHOW DIAGNOSTICS;")
|
||||
influx.query("SHOW DIAGNOSTICS;", database=conf[CONF_DB_NAME])
|
||||
except exceptions.InfluxDBClientError as exc:
|
||||
_LOGGER.error("Database host is not accessible due to '%s', please "
|
||||
"check your entries in the configuration file and that "
|
||||
|
|
|
@ -255,7 +255,7 @@ def async_setup(hass, config):
|
|||
if not light.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
light.async_update_ha_state(True))
|
||||
if hasattr(light, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
|
|
@ -6,6 +6,9 @@ https://home-assistant.io/components/light.lifx/
|
|||
"""
|
||||
import colorsys
|
||||
import logging
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -13,117 +16,91 @@ from homeassistant.components.light import (
|
|||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
|
||||
SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.event import track_time_change
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired)
|
||||
from homeassistant import util
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['liffylights==0.9.4']
|
||||
REQUIREMENTS = ['aiolifx==0.4.2']
|
||||
|
||||
BYTE_MAX = 255
|
||||
UDP_BROADCAST_PORT = 56700
|
||||
|
||||
# Delay (in ms) expected for changes to take effect in the physical bulb
|
||||
BULB_LATENCY = 500
|
||||
|
||||
CONF_BROADCAST = 'broadcast'
|
||||
CONF_SERVER = 'server'
|
||||
|
||||
BYTE_MAX = 255
|
||||
SHORT_MAX = 65535
|
||||
|
||||
TEMP_MAX = 9000
|
||||
TEMP_MAX_HASS = 500
|
||||
TEMP_MIN = 2500
|
||||
TEMP_MIN_HASS = 154
|
||||
|
||||
SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
|
||||
SUPPORT_TRANSITION)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_SERVER, default=None): cv.string,
|
||||
vol.Optional(CONF_BROADCAST, default=None): cv.string,
|
||||
vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string,
|
||||
})
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the LIFX platform."""
|
||||
import aiolifx
|
||||
|
||||
server_addr = config.get(CONF_SERVER)
|
||||
broadcast_addr = config.get(CONF_BROADCAST)
|
||||
|
||||
lifx_library = LIFX(add_devices, server_addr, broadcast_addr)
|
||||
lifx_manager = LIFXManager(hass, async_add_devices)
|
||||
|
||||
# Register our poll service
|
||||
track_time_change(hass, lifx_library.poll, second=[10, 40])
|
||||
coro = hass.loop.create_datagram_endpoint(
|
||||
partial(aiolifx.LifxDiscovery, hass.loop, lifx_manager),
|
||||
local_addr=(server_addr, UDP_BROADCAST_PORT))
|
||||
|
||||
lifx_library.probe()
|
||||
hass.async_add_job(coro)
|
||||
return True
|
||||
|
||||
|
||||
class LIFX(object):
|
||||
"""Representation of a LIFX light."""
|
||||
class LIFXManager(object):
|
||||
"""Representation of all known LIFX entities."""
|
||||
|
||||
def __init__(self, add_devices_callback, server_addr=None,
|
||||
broadcast_addr=None):
|
||||
def __init__(self, hass, async_add_devices):
|
||||
"""Initialize the light."""
|
||||
import liffylights
|
||||
self.entities = {}
|
||||
self.hass = hass
|
||||
self.async_add_devices = async_add_devices
|
||||
|
||||
self._devices = []
|
||||
|
||||
self._add_devices_callback = add_devices_callback
|
||||
|
||||
self._liffylights = liffylights.LiffyLights(
|
||||
self.on_device, self.on_power, self.on_color, server_addr,
|
||||
broadcast_addr)
|
||||
|
||||
def find_bulb(self, ipaddr):
|
||||
"""Search for bulbs."""
|
||||
bulb = None
|
||||
for device in self._devices:
|
||||
if device.ipaddr == ipaddr:
|
||||
bulb = device
|
||||
break
|
||||
return bulb
|
||||
|
||||
def on_device(self, ipaddr, name, power, hue, sat, bri, kel):
|
||||
"""Initialize the light."""
|
||||
bulb = self.find_bulb(ipaddr)
|
||||
|
||||
if bulb is None:
|
||||
_LOGGER.debug("new bulb %s %s %d %d %d %d %d",
|
||||
ipaddr, name, power, hue, sat, bri, kel)
|
||||
bulb = LIFXLight(
|
||||
self._liffylights, ipaddr, name, power, hue, sat, bri, kel)
|
||||
self._devices.append(bulb)
|
||||
self._add_devices_callback([bulb])
|
||||
@callback
|
||||
def register(self, device):
|
||||
"""Callback for newly detected bulb."""
|
||||
if device.mac_addr in self.entities:
|
||||
entity = self.entities[device.mac_addr]
|
||||
_LOGGER.debug("%s register AGAIN", entity.ipaddr)
|
||||
entity.available = True
|
||||
self.hass.async_add_job(entity.async_update_ha_state())
|
||||
else:
|
||||
_LOGGER.debug("update bulb %s %s %d %d %d %d %d",
|
||||
ipaddr, name, power, hue, sat, bri, kel)
|
||||
bulb.set_power(power)
|
||||
bulb.set_color(hue, sat, bri, kel)
|
||||
bulb.schedule_update_ha_state()
|
||||
_LOGGER.debug("%s register NEW", device.ip_addr)
|
||||
device.get_color(self.ready)
|
||||
|
||||
def on_color(self, ipaddr, hue, sat, bri, kel):
|
||||
"""Initialize the light."""
|
||||
bulb = self.find_bulb(ipaddr)
|
||||
@callback
|
||||
def ready(self, device, msg):
|
||||
"""Callback that adds the device once all data is retrieved."""
|
||||
entity = LIFXLight(device)
|
||||
_LOGGER.debug("%s register READY", entity.ipaddr)
|
||||
self.entities[device.mac_addr] = entity
|
||||
self.hass.async_add_job(self.async_add_devices, [entity])
|
||||
|
||||
if bulb is not None:
|
||||
bulb.set_color(hue, sat, bri, kel)
|
||||
bulb.schedule_update_ha_state()
|
||||
|
||||
def on_power(self, ipaddr, power):
|
||||
"""Initialize the light."""
|
||||
bulb = self.find_bulb(ipaddr)
|
||||
|
||||
if bulb is not None:
|
||||
bulb.set_power(power)
|
||||
bulb.schedule_update_ha_state()
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def poll(self, now):
|
||||
"""Polling for the light."""
|
||||
self.probe()
|
||||
|
||||
def probe(self, address=None):
|
||||
"""Probe the light."""
|
||||
self._liffylights.probe(address)
|
||||
@callback
|
||||
def unregister(self, device):
|
||||
"""Callback for disappearing bulb."""
|
||||
if device.mac_addr in self.entities:
|
||||
entity = self.entities[device.mac_addr]
|
||||
_LOGGER.debug("%s unregister", entity.ipaddr)
|
||||
entity.available = False
|
||||
entity.updated_event.set()
|
||||
self.hass.async_add_job(entity.async_update_ha_state())
|
||||
|
||||
|
||||
def convert_rgb_to_hsv(rgb):
|
||||
|
@ -140,31 +117,35 @@ def convert_rgb_to_hsv(rgb):
|
|||
class LIFXLight(Light):
|
||||
"""Representation of a LIFX light."""
|
||||
|
||||
def __init__(self, liffy, ipaddr, name, power, hue, saturation, brightness,
|
||||
kelvin):
|
||||
def __init__(self, device):
|
||||
"""Initialize the light."""
|
||||
_LOGGER.debug("LIFXLight: %s %s", ipaddr, name)
|
||||
|
||||
self._liffylights = liffy
|
||||
self._ip = ipaddr
|
||||
self.set_name(name)
|
||||
self.set_power(power)
|
||||
self.set_color(hue, saturation, brightness, kelvin)
|
||||
self.device = device
|
||||
self.updated_event = asyncio.Event()
|
||||
self.blocker = None
|
||||
self.postponed_update = None
|
||||
self._available = True
|
||||
self.set_power(device.power_level)
|
||||
self.set_color(*device.color)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for LIFX light."""
|
||||
return False
|
||||
def available(self):
|
||||
"""Return the availability of the device."""
|
||||
return self._available
|
||||
|
||||
@available.setter
|
||||
def available(self, value):
|
||||
"""Set the availability of the device."""
|
||||
self._available = value
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
return self.device.label
|
||||
|
||||
@property
|
||||
def ipaddr(self):
|
||||
"""Return the IP address of the device."""
|
||||
return self._ip
|
||||
return self.device.ip_addr[0]
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
|
@ -199,16 +180,47 @@ class LIFXLight(Light):
|
|||
"""Flag supported features."""
|
||||
return SUPPORT_LIFX
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
@callback
|
||||
def update_after_transition(self, now):
|
||||
"""Request new status after completion of the last transition."""
|
||||
self.postponed_update = None
|
||||
self.hass.async_add_job(self.async_update_ha_state(force_refresh=True))
|
||||
|
||||
@callback
|
||||
def unblock_updates(self, now):
|
||||
"""Allow async_update after the new state has settled on the bulb."""
|
||||
self.blocker = None
|
||||
self.hass.async_add_job(self.async_update_ha_state(force_refresh=True))
|
||||
|
||||
def update_later(self, when):
|
||||
"""Block immediate update requests and schedule one for later."""
|
||||
if self.blocker:
|
||||
self.blocker()
|
||||
self.blocker = async_track_point_in_utc_time(
|
||||
self.hass, self.unblock_updates,
|
||||
util.dt.utcnow() + timedelta(milliseconds=BULB_LATENCY))
|
||||
|
||||
if self.postponed_update:
|
||||
self.postponed_update()
|
||||
if when > BULB_LATENCY:
|
||||
self.postponed_update = async_track_point_in_utc_time(
|
||||
self.hass, self.update_after_transition,
|
||||
util.dt.utcnow() + timedelta(milliseconds=when+BULB_LATENCY))
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
||||
else:
|
||||
fade = 0
|
||||
|
||||
changed_color = False
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
hue, saturation, brightness = \
|
||||
convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR])
|
||||
changed_color = True
|
||||
else:
|
||||
hue = self._hue
|
||||
saturation = self._sat
|
||||
|
@ -216,40 +228,64 @@ class LIFXLight(Light):
|
|||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1)
|
||||
changed_color = True
|
||||
else:
|
||||
brightness = self._bri
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
kelvin = int(color_temperature_mired_to_kelvin(
|
||||
kwargs[ATTR_COLOR_TEMP]))
|
||||
changed_color = True
|
||||
else:
|
||||
kelvin = self._kel
|
||||
|
||||
hsbk = [hue, saturation, brightness, kelvin]
|
||||
_LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d",
|
||||
self._ip, self._power,
|
||||
hue, saturation, brightness, kelvin, fade)
|
||||
self.ipaddr, self._power, fade, *hsbk)
|
||||
|
||||
if self._power == 0:
|
||||
self._liffylights.set_color(self._ip, hue, saturation,
|
||||
brightness, kelvin, 0)
|
||||
self._liffylights.set_power(self._ip, 65535, fade)
|
||||
if changed_color:
|
||||
self.device.set_color(hsbk, None, 0)
|
||||
self.device.set_power(True, None, fade)
|
||||
else:
|
||||
self._liffylights.set_color(self._ip, hue, saturation,
|
||||
brightness, kelvin, fade)
|
||||
self.device.set_power(True, None, 0) # racing for power status
|
||||
if changed_color:
|
||||
self.device.set_color(hsbk, None, fade)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
self.update_later(0)
|
||||
if fade < BULB_LATENCY:
|
||||
self.set_power(1)
|
||||
self.set_color(*hsbk)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fade = int(kwargs[ATTR_TRANSITION] * 1000)
|
||||
else:
|
||||
fade = 0
|
||||
|
||||
_LOGGER.debug("turn_off: %s %d", self._ip, fade)
|
||||
self._liffylights.set_power(self._ip, 0, fade)
|
||||
self.device.set_power(False, None, fade)
|
||||
|
||||
def set_name(self, name):
|
||||
"""Set name of the light."""
|
||||
self._name = name
|
||||
self.update_later(fade)
|
||||
if fade < BULB_LATENCY:
|
||||
self.set_power(0)
|
||||
|
||||
@callback
|
||||
def got_color(self, device, msg):
|
||||
"""Callback that gets current power/color status."""
|
||||
self.set_power(device.power_level)
|
||||
self.set_color(*device.color)
|
||||
self.updated_event.set()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update bulb status (if it is available)."""
|
||||
_LOGGER.debug("%s async_update", self.ipaddr)
|
||||
if self.available and self.blocker is None:
|
||||
self.updated_event.clear()
|
||||
self.device.get_color(self.got_color)
|
||||
yield from self.updated_event.wait()
|
||||
|
||||
def set_power(self, power):
|
||||
"""Set power state value."""
|
||||
|
|
64
homeassistant/components/light/lutron_caseta.py
Normal file
64
homeassistant/components/light/lutron_caseta.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
"""Support for Lutron Caseta lights."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
|
||||
from homeassistant.components.light.lutron import (
|
||||
to_hass_level, to_lutron_level)
|
||||
from homeassistant.components.lutron_caseta import (
|
||||
LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['lutron_caseta']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Lutron Caseta lights."""
|
||||
devs = []
|
||||
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
||||
light_devices = bridge.get_devices_by_type("WallDimmer")
|
||||
for light_device in light_devices:
|
||||
dev = LutronCasetaLight(light_device, bridge)
|
||||
devs.append(dev)
|
||||
|
||||
add_devices(devs, True)
|
||||
|
||||
|
||||
class LutronCasetaLight(LutronCasetaDevice, Light):
|
||||
"""Representation of a Lutron Light, including dimmable."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_BRIGHTNESS
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of the light."""
|
||||
return to_hass_level(self._state["current_state"])
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
if ATTR_BRIGHTNESS in kwargs and self._device_type == "WallDimmer":
|
||||
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
else:
|
||||
brightness = 255
|
||||
self._smartbridge.set_value(self._device_id,
|
||||
to_lutron_level(brightness))
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
self._smartbridge.set_value(self._device_id, 0)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
return self._state["current_state"] > 0
|
||||
|
||||
def update(self):
|
||||
"""Called when forcing a refresh of the device."""
|
||||
self._state = self._smartbridge.get_device_by_id(self._device_id)
|
||||
_LOGGER.debug(self._state)
|
|
@ -14,8 +14,8 @@ from homeassistant.components.rflink import (
|
|||
CONF_IGNORE_DEVICES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER,
|
||||
DATA_ENTITY_LOOKUP, DEVICE_DEFAULTS_SCHEMA, DOMAIN,
|
||||
EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv, vol)
|
||||
from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_TYPE
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_PLATFORM, CONF_TYPE, STATE_UNKNOWN)
|
||||
DEPENDENCIES = ['rflink']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -23,6 +23,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
TYPE_DIMMABLE = 'dimmable'
|
||||
TYPE_SWITCHABLE = 'switchable'
|
||||
TYPE_HYBRID = 'hybrid'
|
||||
TYPE_TOGGLE = 'toggle'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): DOMAIN,
|
||||
|
@ -33,7 +34,8 @@ PLATFORM_SCHEMA = vol.Schema({
|
|||
cv.string: {
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_TYPE):
|
||||
vol.Any(TYPE_DIMMABLE, TYPE_SWITCHABLE, TYPE_HYBRID),
|
||||
vol.Any(TYPE_DIMMABLE, TYPE_SWITCHABLE,
|
||||
TYPE_HYBRID, TYPE_TOGGLE),
|
||||
vol.Optional(CONF_ALIASSES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
|
||||
|
@ -71,6 +73,9 @@ def entity_class_for_type(entity_type):
|
|||
# sends 'dim' and 'on' command to support both dimmers and on/off
|
||||
# switches. Not compatible with signal repetition.
|
||||
TYPE_HYBRID: HybridRflinkLight,
|
||||
# sends only 'on' commands for switches which turn on and off
|
||||
# using the same 'on' command for both.
|
||||
TYPE_TOGGLE: ToggleRflinkLight,
|
||||
}
|
||||
|
||||
return entity_device_mapping.get(entity_type, RflinkLight)
|
||||
|
@ -213,3 +218,38 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light):
|
|||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_BRIGHTNESS
|
||||
|
||||
|
||||
class ToggleRflinkLight(SwitchableRflinkDevice, Light):
|
||||
"""Rflink light device which sends out only 'on' commands.
|
||||
|
||||
Some switches like for example Livolo light switches use the
|
||||
same 'on' command to switch on and switch off the lights.
|
||||
If the light is on and 'on' gets sent, the light will turn off
|
||||
and if the light is off and 'on' gets sent, the light will turn on.
|
||||
"""
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
"""Return entity id."""
|
||||
return "light.{}".format(self.name)
|
||||
|
||||
def _handle_event(self, event):
|
||||
"""Adjust state if Rflink picks up a remote command for this device."""
|
||||
self.cancel_queued_send_commands()
|
||||
|
||||
command = event['command']
|
||||
if command == 'on':
|
||||
# if the state is unknown or false, it gets set as true
|
||||
# if the state is true, it gets set as false
|
||||
self._state = self._state in [STATE_UNKNOWN, False]
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
yield from self._async_handle_command('toggle')
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
yield from self._async_handle_command('toggle')
|
||||
|
|
|
@ -18,6 +18,8 @@ DEPENDENCIES = ['wink']
|
|||
|
||||
SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR
|
||||
|
||||
RGB_MODES = ['hsb', 'rgb']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink lights."""
|
||||
|
@ -54,6 +56,8 @@ class WinkLight(WinkDevice, Light):
|
|||
"""Current bulb color in RGB."""
|
||||
if not self.wink.supports_hue_saturation():
|
||||
return None
|
||||
elif self.wink.color_model() not in RGB_MODES:
|
||||
return False
|
||||
else:
|
||||
hue = self.wink.color_hue()
|
||||
saturation = self.wink.color_saturation()
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.yeelightsunflower
|
||||
https://home-assistant.io/components/light.yeelightsunflower/
|
||||
"""
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
|
|
@ -49,9 +49,9 @@ SUPPORT_ZWAVE_COLORTEMP = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR
|
|||
| SUPPORT_COLOR_TEMP)
|
||||
|
||||
|
||||
def get_device(node, value, node_config, **kwargs):
|
||||
def get_device(node, values, node_config, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
name = '{}.{}'.format(DOMAIN, zwave.object_id(value))
|
||||
name = '{}.{}'.format(DOMAIN, zwave.object_id(values.primary))
|
||||
refresh = node_config.get(zwave.CONF_REFRESH_VALUE)
|
||||
delay = node_config.get(zwave.CONF_REFRESH_DELAY)
|
||||
_LOGGER.debug('name=%s node_config=%s CONF_REFRESH_VALUE=%s'
|
||||
|
@ -59,9 +59,9 @@ def get_device(node, value, node_config, **kwargs):
|
|||
refresh, delay)
|
||||
|
||||
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR):
|
||||
return ZwaveColorLight(value, refresh, delay)
|
||||
return ZwaveColorLight(values, refresh, delay)
|
||||
else:
|
||||
return ZwaveDimmer(value, refresh, delay)
|
||||
return ZwaveDimmer(values, refresh, delay)
|
||||
|
||||
|
||||
def brightness_state(value):
|
||||
|
@ -75,9 +75,9 @@ def brightness_state(value):
|
|||
class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
||||
"""Representation of a Z-Wave dimmer."""
|
||||
|
||||
def __init__(self, value, refresh, delay):
|
||||
def __init__(self, values, refresh, delay):
|
||||
"""Initialize the light."""
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._brightness = None
|
||||
self._state = None
|
||||
self._delay = delay
|
||||
|
@ -86,10 +86,10 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||
|
||||
# Enable appropriate workaround flags for our device
|
||||
# Make sure that we have values for the key before converting to int
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_id, 16))
|
||||
if (self.node.manufacturer_id.strip() and
|
||||
self.node.product_id.strip()):
|
||||
specific_sensor_key = (int(self.node.manufacturer_id, 16),
|
||||
int(self.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")
|
||||
|
@ -105,7 +105,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||
def update_properties(self):
|
||||
"""Update internal properties based on zwave values."""
|
||||
# Brightness
|
||||
self._brightness, self._state = brightness_state(self._value)
|
||||
self._brightness, self._state = brightness_state(self.values.primary)
|
||||
|
||||
def value_changed(self):
|
||||
"""Called when a value for this entity's node has changed."""
|
||||
|
@ -116,7 +116,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||
def _refresh_value():
|
||||
"""Used timer callback for delayed value refresh."""
|
||||
self._refreshing = True
|
||||
self._value.refresh()
|
||||
self.values.primary.refresh()
|
||||
|
||||
if self._timer is not None and self._timer.isAlive():
|
||||
self._timer.cancel()
|
||||
|
@ -151,12 +151,12 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
|
|||
else:
|
||||
brightness = 255
|
||||
|
||||
if self._value.node.set_dimmer(self._value.value_id, brightness):
|
||||
if self.node.set_dimmer(self.values.primary.value_id, brightness):
|
||||
self._state = STATE_ON
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
if self._value.node.set_dimmer(self._value.value_id, 0):
|
||||
if self.node.set_dimmer(self.values.primary.value_id, 0):
|
||||
self._state = STATE_OFF
|
||||
|
||||
|
||||
|
@ -170,73 +170,28 @@ def ct_to_rgb(temp):
|
|||
class ZwaveColorLight(ZwaveDimmer):
|
||||
"""Representation of a Z-Wave color changing light."""
|
||||
|
||||
def __init__(self, value, refresh, delay):
|
||||
def __init__(self, values, refresh, delay):
|
||||
"""Initialize the light."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
|
||||
self._value_color = None
|
||||
self._value_color_channels = None
|
||||
self._color_channels = None
|
||||
self._rgb = None
|
||||
self._ct = None
|
||||
|
||||
super().__init__(value, refresh, delay)
|
||||
|
||||
# Create a listener so the color values can be linked to this entity
|
||||
dispatcher.connect(
|
||||
self._value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED)
|
||||
self._get_color_values()
|
||||
|
||||
@property
|
||||
def dependent_value_ids(self):
|
||||
"""List of value IDs a device depends on."""
|
||||
return [val.value_id for val in [
|
||||
self._value_color, self._value_color_channels] if val]
|
||||
|
||||
def _get_color_values(self):
|
||||
"""Search for color values available on this node."""
|
||||
from openzwave.network import ZWaveNetwork
|
||||
from pydispatch import dispatcher
|
||||
|
||||
_LOGGER.debug("Searching for zwave color values")
|
||||
# Currently zwave nodes only exist with one color element per node.
|
||||
if self._value_color is None:
|
||||
for value_color in self._value.node.get_rgbbulbs().values():
|
||||
self._value_color = value_color
|
||||
|
||||
if self._value_color_channels is None:
|
||||
self._value_color_channels = self.get_value(
|
||||
class_id=zwave.const.COMMAND_CLASS_SWITCH_COLOR,
|
||||
genre=zwave.const.GENRE_SYSTEM, type=zwave.const.TYPE_INT)
|
||||
|
||||
if self._value_color and self._value_color_channels:
|
||||
_LOGGER.debug("Zwave node color values found.")
|
||||
dispatcher.disconnect(
|
||||
self._value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED)
|
||||
self.update_properties()
|
||||
|
||||
def _value_added(self, value):
|
||||
"""Called when a value has been added to the network."""
|
||||
if self._value.node != value.node:
|
||||
return
|
||||
# Check for the missing color values
|
||||
self._get_color_values()
|
||||
super().__init__(values, refresh, delay)
|
||||
|
||||
def update_properties(self):
|
||||
"""Update internal properties based on zwave values."""
|
||||
super().update_properties()
|
||||
|
||||
if self._value_color is None:
|
||||
if self.values.color is None:
|
||||
return
|
||||
if self._value_color_channels is None:
|
||||
if self.values.color_channels is None:
|
||||
return
|
||||
|
||||
# Color Channels
|
||||
self._color_channels = self._value_color_channels.data
|
||||
self._color_channels = self.values.color_channels.data
|
||||
|
||||
# Color Data String
|
||||
data = self._value_color.data
|
||||
data = self.values.color.data
|
||||
|
||||
# RGB is always present in the openzwave color data string.
|
||||
self._rgb = [
|
||||
|
@ -309,10 +264,10 @@ class ZwaveColorLight(ZwaveDimmer):
|
|||
if self._zw098:
|
||||
if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS:
|
||||
self._ct = TEMP_WARM_HASS
|
||||
rgbw = b'#000000FF00'
|
||||
rgbw = b'#000000ff00'
|
||||
else:
|
||||
self._ct = TEMP_COLD_HASS
|
||||
rgbw = b'#00000000FF'
|
||||
rgbw = b'#00000000ff'
|
||||
|
||||
elif ATTR_RGB_COLOR in kwargs:
|
||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||
|
@ -329,8 +284,8 @@ class ZwaveColorLight(ZwaveDimmer):
|
|||
rgbw += format(colorval, '02x').encode('utf-8')
|
||||
rgbw += b'0000'
|
||||
|
||||
if rgbw and self._value_color:
|
||||
self._value_color.node.set_rgbw(self._value_color.value_id, rgbw)
|
||||
if rgbw and self.values.color:
|
||||
self.values.color.data = rgbw
|
||||
|
||||
super().turn_on(**kwargs)
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ def async_setup(hass, config):
|
|||
if not entity.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
entity.async_update_ha_state(True))
|
||||
if hasattr(entity, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
|
|
@ -6,6 +6,7 @@ https://home-assistant.io/components/lock.zwave/
|
|||
"""
|
||||
# Because we do not compile openzwave on CI
|
||||
# pylint: disable=import-error
|
||||
import asyncio
|
||||
import logging
|
||||
from os import path
|
||||
|
||||
|
@ -13,7 +14,6 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.lock import DOMAIN, LockDevice
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
@ -53,7 +53,7 @@ LOCK_ALARM_TYPE = {
|
|||
'9': 'Deadbolt Jammed',
|
||||
'18': 'Locked with Keypad by user ',
|
||||
'19': 'Unlocked with Keypad by user ',
|
||||
'21': 'Manually Locked by',
|
||||
'21': 'Manually Locked by ',
|
||||
'22': 'Manually Unlocked by Key or Inside thumb turn',
|
||||
'24': 'Locked by RF',
|
||||
'25': 'Unlocked by RF',
|
||||
|
@ -120,8 +120,12 @@ CLEAR_USERCODE_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
|
||||
def get_device(hass, node, value, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Generic Z-Wave platform setup."""
|
||||
yield from zwave.async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
path.join(path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
|
@ -140,6 +144,7 @@ def get_device(hass, node, value, **kwargs):
|
|||
_LOGGER.error('Invalid code provided: (%s)'
|
||||
' usercode must %s or less digits',
|
||||
usercode, len(value.data))
|
||||
break
|
||||
value.data = str(usercode)
|
||||
break
|
||||
|
||||
|
@ -175,32 +180,34 @@ def get_device(hass, node, value, **kwargs):
|
|||
_LOGGER.info('Usercode at slot %s is cleared', value.index)
|
||||
break
|
||||
|
||||
if node.has_command_class(zwave.const.COMMAND_CLASS_USER_CODE):
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_SET_USERCODE,
|
||||
set_usercode,
|
||||
descriptions.get(SERVICE_SET_USERCODE),
|
||||
schema=SET_USERCODE_SCHEMA)
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_GET_USERCODE,
|
||||
get_usercode,
|
||||
descriptions.get(SERVICE_GET_USERCODE),
|
||||
schema=GET_USERCODE_SCHEMA)
|
||||
hass.services.register(DOMAIN,
|
||||
SERVICE_CLEAR_USERCODE,
|
||||
clear_usercode,
|
||||
descriptions.get(SERVICE_CLEAR_USERCODE),
|
||||
schema=CLEAR_USERCODE_SCHEMA)
|
||||
return ZwaveLock(value)
|
||||
hass.services.async_register(DOMAIN,
|
||||
SERVICE_SET_USERCODE,
|
||||
set_usercode,
|
||||
descriptions.get(SERVICE_SET_USERCODE),
|
||||
schema=SET_USERCODE_SCHEMA)
|
||||
hass.services.async_register(DOMAIN,
|
||||
SERVICE_GET_USERCODE,
|
||||
get_usercode,
|
||||
descriptions.get(SERVICE_GET_USERCODE),
|
||||
schema=GET_USERCODE_SCHEMA)
|
||||
hass.services.async_register(DOMAIN,
|
||||
SERVICE_CLEAR_USERCODE,
|
||||
clear_usercode,
|
||||
descriptions.get(SERVICE_CLEAR_USERCODE),
|
||||
schema=CLEAR_USERCODE_SCHEMA)
|
||||
|
||||
|
||||
def get_device(node, values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
return ZwaveLock(values)
|
||||
|
||||
|
||||
class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
||||
"""Representation of a Z-Wave Lock."""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, values):
|
||||
"""Initialize the Z-Wave lock device."""
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
self._node = value.node
|
||||
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self._state = None
|
||||
self._notification = None
|
||||
self._lock_status = None
|
||||
|
@ -208,10 +215,10 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
|||
|
||||
# Enable appropriate workaround flags for our device
|
||||
# Make sure that we have values for the key before converting to int
|
||||
if (value.node.manufacturer_id.strip() and
|
||||
value.node.product_id.strip()):
|
||||
specific_sensor_key = (int(value.node.manufacturer_id, 16),
|
||||
int(value.node.product_id, 16))
|
||||
if (self.node.manufacturer_id.strip() and
|
||||
self.node.product_id.strip()):
|
||||
specific_sensor_key = (int(self.node.manufacturer_id, 16),
|
||||
int(self.node.product_id, 16))
|
||||
if specific_sensor_key in DEVICE_MAPPINGS:
|
||||
if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_V2BTZE:
|
||||
self._v2btze = 1
|
||||
|
@ -221,43 +228,41 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
|||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
self._state = self._value.data
|
||||
self._state = self.values.primary.data
|
||||
_LOGGER.debug('Lock state set from Bool value and'
|
||||
' is %s', self._state)
|
||||
notification_data = self.get_value(class_id=zwave.const
|
||||
.COMMAND_CLASS_ALARM,
|
||||
label=['Access Control'],
|
||||
member='data')
|
||||
if notification_data:
|
||||
if self.values.access_control:
|
||||
notification_data = self.values.access_control.data
|
||||
self._notification = LOCK_NOTIFICATION.get(str(notification_data))
|
||||
if self._v2btze:
|
||||
advanced_config = self.get_value(class_id=zwave.const
|
||||
.COMMAND_CLASS_CONFIGURATION,
|
||||
index=12,
|
||||
data=CONFIG_ADVANCED,
|
||||
member='data')
|
||||
if advanced_config:
|
||||
self._state = LOCK_STATUS.get(str(notification_data))
|
||||
_LOGGER.debug('Lock state set from Access Control '
|
||||
'value and is %s, get=%s',
|
||||
str(notification_data),
|
||||
self.state)
|
||||
|
||||
alarm_type = self.get_value(class_id=zwave.const
|
||||
.COMMAND_CLASS_ALARM,
|
||||
label=['Alarm Type'], member='data')
|
||||
if self._v2btze:
|
||||
if self.values.v2btze_advanced and \
|
||||
self.values.v2btze_advanced.data == CONFIG_ADVANCED:
|
||||
self._state = LOCK_STATUS.get(str(notification_data))
|
||||
_LOGGER.debug('Lock state set from Access Control '
|
||||
'value and is %s, get=%s',
|
||||
str(notification_data),
|
||||
self.state)
|
||||
|
||||
if not self.values.alarm_type:
|
||||
return
|
||||
|
||||
alarm_type = self.values.alarm_type.data
|
||||
_LOGGER.debug('Lock alarm_type is %s', str(alarm_type))
|
||||
alarm_level = self.get_value(class_id=zwave.const
|
||||
.COMMAND_CLASS_ALARM,
|
||||
label=['Alarm Level'], member='data')
|
||||
if self.values.alarm_level:
|
||||
alarm_level = self.values.alarm_level.data
|
||||
else:
|
||||
alarm_level = None
|
||||
_LOGGER.debug('Lock alarm_level is %s', str(alarm_level))
|
||||
|
||||
if not alarm_type:
|
||||
return
|
||||
if alarm_type is 21:
|
||||
self._lock_status = '{}{}'.format(
|
||||
LOCK_ALARM_TYPE.get(str(alarm_type)),
|
||||
MANUAL_LOCK_ALARM_LEVEL.get(str(alarm_level)))
|
||||
if alarm_type in ALARM_TYPE_STD:
|
||||
return
|
||||
if str(alarm_type) in ALARM_TYPE_STD:
|
||||
self._lock_status = '{}{}'.format(
|
||||
LOCK_ALARM_TYPE.get(str(alarm_type)), str(alarm_level))
|
||||
return
|
||||
|
@ -277,11 +282,11 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
|||
|
||||
def lock(self, **kwargs):
|
||||
"""Lock the device."""
|
||||
self._value.data = True
|
||||
self.values.primary.data = True
|
||||
|
||||
def unlock(self, **kwargs):
|
||||
"""Unlock the device."""
|
||||
self._value.data = False
|
||||
self.values.primary.data = False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
@ -292,8 +297,3 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice):
|
|||
if self._lock_status:
|
||||
data[ATTR_LOCK_STATUS] = self._lock_status
|
||||
return data
|
||||
|
||||
@property
|
||||
def dependent_value_ids(self):
|
||||
"""List of value IDs a device depends on."""
|
||||
return None
|
||||
|
|
100
homeassistant/components/lutron_caseta.py
Normal file
100
homeassistant/components/lutron_caseta.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
"""
|
||||
Component for interacting with a Lutron Caseta system.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/lutron_caseta/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (CONF_HOST,
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD)
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['https://github.com/gurumitts/'
|
||||
'pylutron-caseta/archive/v0.2.4.zip#'
|
||||
'pylutron-caseta==v0.2.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
LUTRON_CASETA_SMARTBRIDGE = 'lutron_smartbridge'
|
||||
|
||||
DOMAIN = 'lutron_caseta'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, base_config):
|
||||
"""Setup the Lutron component."""
|
||||
from pylutron_caseta.smartbridge import Smartbridge
|
||||
|
||||
config = base_config.get(DOMAIN)
|
||||
hass.data[LUTRON_CASETA_SMARTBRIDGE] = Smartbridge(
|
||||
hostname=config[CONF_HOST],
|
||||
username=config[CONF_USERNAME],
|
||||
password=config[CONF_PASSWORD]
|
||||
)
|
||||
if not hass.data[LUTRON_CASETA_SMARTBRIDGE].is_connected():
|
||||
_LOGGER.error("Unable to connect to Lutron smartbridge at %s",
|
||||
config[CONF_HOST])
|
||||
return False
|
||||
|
||||
_LOGGER.info("Connected to Lutron smartbridge at %s",
|
||||
config[CONF_HOST])
|
||||
|
||||
for component in ('light', 'switch'):
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class LutronCasetaDevice(Entity):
|
||||
"""Common base class for all Lutron Caseta devices."""
|
||||
|
||||
def __init__(self, device, bridge):
|
||||
"""Set up the base class.
|
||||
|
||||
[:param]device the device metadata
|
||||
[:param]bridge the smartbridge object
|
||||
"""
|
||||
self._device_id = device["device_id"]
|
||||
self._device_type = device["type"]
|
||||
self._device_name = device["name"]
|
||||
self._state = None
|
||||
self._smartbridge = bridge
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._smartbridge.add_subscriber(self._device_id,
|
||||
self._update_callback)
|
||||
|
||||
def _update_callback(self):
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._device_name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = {'Lutron Integration ID': self._device_id}
|
||||
return attr
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
|
@ -4,32 +4,37 @@ Support to interface with the Emby API.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.emby/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
|
||||
SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice,
|
||||
SUPPORT_PLAY, PLATFORM_SCHEMA)
|
||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
|
||||
MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_API_KEY, CONF_PORT, CONF_SSL, DEVICE_DEFAULT_NAME,
|
||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN)
|
||||
from homeassistant.helpers.event import (track_utc_time_change)
|
||||
from homeassistant.util import Throttle
|
||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
||||
CONF_HOST, CONF_PORT, CONF_SSL, CONF_API_KEY, DEVICE_DEFAULT_NAME,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['pyemby==0.2']
|
||||
REQUIREMENTS = ['pyemby==1.1']
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTO_HIDE = 'auto_hide'
|
||||
|
||||
MEDIA_TYPE_TRAILER = 'trailer'
|
||||
MEDIA_TYPE_GENERIC_VIDEO = 'video'
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 8096
|
||||
DEFAULT_SSL_PORT = 8920
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_AUTO_HIDE = False
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -37,219 +42,210 @@ SUPPORT_EMBY = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
|||
SUPPORT_STOP | SUPPORT_SEEK | SUPPORT_PLAY
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default='localhost'): cv.string,
|
||||
vol.Optional(CONF_SSL, default=False): cv.boolean,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_PORT, default=None): cv.port,
|
||||
vol.Optional(CONF_AUTO_HIDE, default=DEFAULT_AUTO_HIDE): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the Emby platform."""
|
||||
from pyemby.emby import EmbyRemote
|
||||
from pyemby import EmbyServer
|
||||
|
||||
_host = config.get(CONF_HOST)
|
||||
_key = config.get(CONF_API_KEY)
|
||||
_port = config.get(CONF_PORT)
|
||||
host = config.get(CONF_HOST)
|
||||
key = config.get(CONF_API_KEY)
|
||||
port = config.get(CONF_PORT)
|
||||
ssl = config.get(CONF_SSL)
|
||||
auto_hide = config.get(CONF_AUTO_HIDE)
|
||||
|
||||
if config.get(CONF_SSL):
|
||||
_protocol = "https"
|
||||
else:
|
||||
_protocol = "http"
|
||||
if port is None:
|
||||
port = DEFAULT_SSL_PORT if ssl else DEFAULT_PORT
|
||||
|
||||
_url = '{}://{}:{}'.format(_protocol, _host, _port)
|
||||
_LOGGER.debug('Setting up Emby server at: %s:%s', host, port)
|
||||
|
||||
_LOGGER.debug('Setting up Emby server at: %s', _url)
|
||||
emby = EmbyServer(host, key, port, ssl, hass.loop)
|
||||
|
||||
embyserver = EmbyRemote(_key, _url)
|
||||
active_emby_devices = {}
|
||||
inactive_emby_devices = {}
|
||||
|
||||
emby_clients = {}
|
||||
emby_sessions = {}
|
||||
track_utc_time_change(hass, lambda now: update_devices(), second=30)
|
||||
@callback
|
||||
def device_update_callback(data):
|
||||
"""Callback for when devices are added to emby."""
|
||||
new_devices = []
|
||||
active_devices = []
|
||||
for dev_id in emby.devices:
|
||||
active_devices.append(dev_id)
|
||||
if dev_id not in active_emby_devices and \
|
||||
dev_id not in inactive_emby_devices:
|
||||
new = EmbyDevice(emby, dev_id)
|
||||
active_emby_devices[dev_id] = new
|
||||
new_devices.append(new)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_devices():
|
||||
"""Update the devices objects."""
|
||||
devices = embyserver.get_sessions()
|
||||
if devices is None:
|
||||
_LOGGER.error('Error listing Emby devices.')
|
||||
return
|
||||
elif dev_id in inactive_emby_devices:
|
||||
if emby.devices[dev_id].state != 'Off':
|
||||
add = inactive_emby_devices.pop(dev_id)
|
||||
active_emby_devices[dev_id] = add
|
||||
_LOGGER.debug("Showing %s, item: %s", dev_id, add)
|
||||
add.set_available(True)
|
||||
add.set_hidden(False)
|
||||
|
||||
new_emby_clients = []
|
||||
for device in devices:
|
||||
if device['DeviceId'] == embyserver.unique_id:
|
||||
break
|
||||
if new_devices:
|
||||
_LOGGER.debug("Adding new devices to HASS: %s", new_devices)
|
||||
async_add_devices(new_devices, update_before_add=True)
|
||||
|
||||
if device['DeviceId'] not in emby_clients:
|
||||
_LOGGER.debug('New Emby DeviceID: %s. Adding to Clients.',
|
||||
device['DeviceId'])
|
||||
new_client = EmbyClient(embyserver, device, emby_sessions,
|
||||
update_devices, update_sessions)
|
||||
emby_clients[device['DeviceId']] = new_client
|
||||
new_emby_clients.append(new_client)
|
||||
else:
|
||||
emby_clients[device['DeviceId']].set_device(device)
|
||||
@callback
|
||||
def device_removal_callback(data):
|
||||
"""Callback for when devices are removed from emby."""
|
||||
if data in active_emby_devices:
|
||||
rem = active_emby_devices.pop(data)
|
||||
inactive_emby_devices[data] = rem
|
||||
_LOGGER.debug("Inactive %s, item: %s", data, rem)
|
||||
rem.set_available(False)
|
||||
if auto_hide:
|
||||
rem.set_hidden(True)
|
||||
|
||||
if new_emby_clients:
|
||||
add_devices_callback(new_emby_clients)
|
||||
@callback
|
||||
def start_emby(event):
|
||||
"""Start emby connection."""
|
||||
emby.start()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_sessions():
|
||||
"""Update the sessions objects."""
|
||||
sessions = embyserver.get_sessions()
|
||||
if sessions is None:
|
||||
_LOGGER.error('Error listing Emby sessions')
|
||||
return
|
||||
@asyncio.coroutine
|
||||
def stop_emby(event):
|
||||
"""Stop emby connection."""
|
||||
yield from emby.stop()
|
||||
|
||||
emby_sessions.clear()
|
||||
for session in sessions:
|
||||
emby_sessions[session['DeviceId']] = session
|
||||
emby.add_new_devices_callback(device_update_callback)
|
||||
emby.add_stale_devices_callback(device_removal_callback)
|
||||
|
||||
update_devices()
|
||||
update_sessions()
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_emby)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_emby)
|
||||
|
||||
|
||||
class EmbyClient(MediaPlayerDevice):
|
||||
"""Representation of a Emby device."""
|
||||
class EmbyDevice(MediaPlayerDevice):
|
||||
"""Representation of an Emby device."""
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-public-methods,
|
||||
|
||||
def __init__(self, client, device, emby_sessions, update_devices,
|
||||
update_sessions):
|
||||
def __init__(self, emby, device_id):
|
||||
"""Initialize the Emby device."""
|
||||
self.emby_sessions = emby_sessions
|
||||
self.update_devices = update_devices
|
||||
self.update_sessions = update_sessions
|
||||
self.client = client
|
||||
self.set_device(device)
|
||||
_LOGGER.debug('New Emby Device initialized with ID: %s', device_id)
|
||||
self.emby = emby
|
||||
self.device_id = device_id
|
||||
self.device = self.emby.devices[self.device_id]
|
||||
|
||||
self._hidden = False
|
||||
self._available = True
|
||||
|
||||
self.media_status_last_position = None
|
||||
self.media_status_received = None
|
||||
|
||||
def set_device(self, device):
|
||||
"""Set the device property."""
|
||||
self.device = device
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callback."""
|
||||
self.emby.add_update_callback(self.async_update_callback,
|
||||
self.device_id)
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, msg):
|
||||
"""Callback for device updates."""
|
||||
# Check if we should update progress
|
||||
if self.device.media_position:
|
||||
if self.device.media_position != self.media_status_last_position:
|
||||
self.media_status_last_position = self.device.media_position
|
||||
self.media_status_received = dt_util.utcnow()
|
||||
elif not self.device.is_nowplaying:
|
||||
# No position, but we have an old value and are still playing
|
||||
self.media_status_last_position = None
|
||||
self.media_status_received = None
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
"""Return True if entity should be hidden from UI."""
|
||||
return self._hidden
|
||||
|
||||
def set_hidden(self, value):
|
||||
"""Set hidden property."""
|
||||
self._hidden = value
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
def set_available(self, value):
|
||||
"""Set available property."""
|
||||
self._available = value
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the id of this emby client."""
|
||||
return '{}.{}'.format(
|
||||
self.__class__, self.device['DeviceId'])
|
||||
return '{}.{}'.format(self.__class__, self.device_id)
|
||||
|
||||
@property
|
||||
def supports_remote_control(self):
|
||||
"""Return control ability."""
|
||||
return self.device['SupportsRemoteControl']
|
||||
return self.device.supports_remote_control
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return 'emby_{}'.format(self.device['DeviceName']) or \
|
||||
DEVICE_DEFAULT_NAME
|
||||
return 'Emby - {} - {}'.format(self.device.client, self.device.name) \
|
||||
or DEVICE_DEFAULT_NAME
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
"""Return the session, if any."""
|
||||
if self.device['DeviceId'] not in self.emby_sessions:
|
||||
return None
|
||||
|
||||
return self.emby_sessions[self.device['DeviceId']]
|
||||
|
||||
@property
|
||||
def now_playing_item(self):
|
||||
"""Return the currently playing item, if any."""
|
||||
session = self.session
|
||||
if session is not None and 'NowPlayingItem' in session:
|
||||
return session['NowPlayingItem']
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
session = self.session
|
||||
if session:
|
||||
if 'NowPlayingItem' in session:
|
||||
if session['PlayState']['IsPaused']:
|
||||
return STATE_PAUSED
|
||||
else:
|
||||
return STATE_PLAYING
|
||||
else:
|
||||
return STATE_IDLE
|
||||
# This is nasty. Need to find a way to determine alive
|
||||
else:
|
||||
state = self.device.state
|
||||
if state == 'Paused':
|
||||
return STATE_PAUSED
|
||||
elif state == 'Playing':
|
||||
return STATE_PLAYING
|
||||
elif state == 'Idle':
|
||||
return STATE_IDLE
|
||||
elif state == 'Off':
|
||||
return STATE_OFF
|
||||
|
||||
return STATE_UNKNOWN
|
||||
|
||||
def update(self):
|
||||
"""Get the latest details."""
|
||||
self.update_devices(no_throttle=True)
|
||||
self.update_sessions(no_throttle=True)
|
||||
# Check if we should update progress
|
||||
try:
|
||||
position = self.session['PlayState']['PositionTicks']
|
||||
except (KeyError, TypeError):
|
||||
self.media_status_last_position = None
|
||||
self.media_status_received = None
|
||||
else:
|
||||
position = int(position) / 10000000
|
||||
if position != self.media_status_last_position:
|
||||
self.media_status_last_position = position
|
||||
self.media_status_received = dt_util.utcnow()
|
||||
|
||||
def play_percent(self):
|
||||
"""Return current media percent complete."""
|
||||
if self.now_playing_item['RunTimeTicks'] and \
|
||||
self.session['PlayState']['PositionTicks']:
|
||||
try:
|
||||
return int(self.session['PlayState']['PositionTicks']) / \
|
||||
int(self.now_playing_item['RunTimeTicks']) * 100
|
||||
except KeyError:
|
||||
return 0
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def app_name(self):
|
||||
"""Return current user as app_name."""
|
||||
# Ideally the media_player object would have a user property.
|
||||
try:
|
||||
return self.device['UserName']
|
||||
except KeyError:
|
||||
return None
|
||||
return self.device.username
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing media."""
|
||||
if self.now_playing_item is not None:
|
||||
try:
|
||||
return self.now_playing_item['Id']
|
||||
except KeyError:
|
||||
return None
|
||||
return self.device.media_id
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
if self.now_playing_item is None:
|
||||
return None
|
||||
try:
|
||||
media_type = self.now_playing_item['Type']
|
||||
if media_type == 'Episode':
|
||||
return MEDIA_TYPE_TVSHOW
|
||||
elif media_type == 'Movie':
|
||||
return MEDIA_TYPE_VIDEO
|
||||
elif media_type == 'Trailer':
|
||||
return MEDIA_TYPE_TRAILER
|
||||
return None
|
||||
except KeyError:
|
||||
return None
|
||||
media_type = self.device.media_type
|
||||
if media_type == 'Episode':
|
||||
return MEDIA_TYPE_TVSHOW
|
||||
elif media_type == 'Movie':
|
||||
return MEDIA_TYPE_VIDEO
|
||||
elif media_type == 'Trailer':
|
||||
return MEDIA_TYPE_TRAILER
|
||||
elif media_type == 'Music':
|
||||
return MEDIA_TYPE_MUSIC
|
||||
elif media_type == 'Video':
|
||||
return MEDIA_TYPE_GENERIC_VIDEO
|
||||
elif media_type == 'Audio':
|
||||
return MEDIA_TYPE_MUSIC
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
if self.now_playing_item and self.media_content_type:
|
||||
try:
|
||||
return int(self.now_playing_item['RunTimeTicks']) / 10000000
|
||||
except KeyError:
|
||||
return None
|
||||
return self.device.media_runtime
|
||||
|
||||
@property
|
||||
def media_position(self):
|
||||
|
@ -268,45 +264,42 @@ class EmbyClient(MediaPlayerDevice):
|
|||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
if self.now_playing_item is not None:
|
||||
try:
|
||||
return self.client.get_image(
|
||||
self.now_playing_item['ThumbItemId'], 'Thumb', 0)
|
||||
except KeyError:
|
||||
try:
|
||||
return self.client.get_image(
|
||||
self.now_playing_item[
|
||||
'PrimaryImageItemId'], 'Primary', 0)
|
||||
except KeyError:
|
||||
return None
|
||||
return self.device.media_image_url
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
# find a string we can use as a title
|
||||
if self.now_playing_item is not None:
|
||||
return self.now_playing_item['Name']
|
||||
return self.device.media_title
|
||||
|
||||
@property
|
||||
def media_season(self):
|
||||
"""Season of curent playing media (TV Show only)."""
|
||||
if self.now_playing_item is not None and \
|
||||
'ParentIndexNumber' in self.now_playing_item:
|
||||
return self.now_playing_item['ParentIndexNumber']
|
||||
return self.device.media_season
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
"""The title of the series of current playing media (TV Show only)."""
|
||||
if self.now_playing_item is not None and \
|
||||
'SeriesName' in self.now_playing_item:
|
||||
return self.now_playing_item['SeriesName']
|
||||
return self.device.media_series_title
|
||||
|
||||
@property
|
||||
def media_episode(self):
|
||||
"""Episode of current playing media (TV Show only)."""
|
||||
if self.now_playing_item is not None and \
|
||||
'IndexNumber' in self.now_playing_item:
|
||||
return self.now_playing_item['IndexNumber']
|
||||
return self.device.media_episode
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Album name of current playing media (Music track only)."""
|
||||
return self.device.media_album_name
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media (Music track only)."""
|
||||
return self.device.media_artist
|
||||
|
||||
@property
|
||||
def media_album_artist(self):
|
||||
"""Album artist of current playing media (Music track only)."""
|
||||
return self.device.media_album_artist
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
@ -316,20 +309,44 @@ class EmbyClient(MediaPlayerDevice):
|
|||
else:
|
||||
return None
|
||||
|
||||
def media_play(self):
|
||||
"""Send play command."""
|
||||
if self.supports_remote_control:
|
||||
self.client.play(self.session)
|
||||
def async_media_play(self):
|
||||
"""Play media.
|
||||
|
||||
def media_pause(self):
|
||||
"""Send pause command."""
|
||||
if self.supports_remote_control:
|
||||
self.client.pause(self.session)
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.device.media_play()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self.client.next_track(self.session)
|
||||
def async_media_pause(self):
|
||||
"""Pause the media player.
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send previous track command."""
|
||||
self.client.previous_track(self.session)
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.device.media_pause()
|
||||
|
||||
def async_media_stop(self):
|
||||
"""Stop the media player.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.device.media_stop()
|
||||
|
||||
def async_media_next_track(self):
|
||||
"""Send next track command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.device.media_next()
|
||||
|
||||
def async_media_previous_track(self):
|
||||
"""Send next track command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.device.media_previous()
|
||||
|
||||
def async_media_seek(self, position):
|
||||
"""Send seek command.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.device.media_seek(position)
|
||||
|
|
|
@ -16,16 +16,18 @@ from homeassistant.components.media_player import (
|
|||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
||||
SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice,
|
||||
PLATFORM_SCHEMA)
|
||||
PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO,
|
||||
MEDIA_TYPE_PLAYLIST)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME,
|
||||
CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_PORT, CONF_SSL, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import get_deprecated
|
||||
|
||||
REQUIREMENTS = ['jsonrpc-async==0.4', 'jsonrpc-websocket==0.2']
|
||||
REQUIREMENTS = ['jsonrpc-async==0.4', 'jsonrpc-websocket==0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -37,11 +39,26 @@ DEFAULT_NAME = 'Kodi'
|
|||
DEFAULT_PORT = 8080
|
||||
DEFAULT_TCP_PORT = 9090
|
||||
DEFAULT_TIMEOUT = 5
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_PROXY_SSL = False
|
||||
DEFAULT_ENABLE_WEBSOCKET = True
|
||||
|
||||
TURN_OFF_ACTION = [None, 'quit', 'hibernate', 'suspend', 'reboot', 'shutdown']
|
||||
|
||||
# https://github.com/xbmc/xbmc/blob/master/xbmc/media/MediaType.h
|
||||
MEDIA_TYPES = {
|
||||
"music": MEDIA_TYPE_MUSIC,
|
||||
"artist": MEDIA_TYPE_MUSIC,
|
||||
"album": MEDIA_TYPE_MUSIC,
|
||||
"song": MEDIA_TYPE_MUSIC,
|
||||
"video": MEDIA_TYPE_VIDEO,
|
||||
"set": MEDIA_TYPE_PLAYLIST,
|
||||
"musicvideo": MEDIA_TYPE_VIDEO,
|
||||
"movie": MEDIA_TYPE_VIDEO,
|
||||
"tvshow": MEDIA_TYPE_TVSHOW,
|
||||
"season": MEDIA_TYPE_TVSHOW,
|
||||
"episode": MEDIA_TYPE_TVSHOW,
|
||||
}
|
||||
|
||||
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
||||
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_VOLUME_STEP
|
||||
|
@ -51,7 +68,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
vol.Optional(CONF_PROXY_SSL, default=DEFAULT_PROXY_SSL): cv.boolean,
|
||||
vol.Optional(CONF_TURN_OFF_ACTION, default=None): vol.In(TURN_OFF_ACTION),
|
||||
vol.Inclusive(CONF_USERNAME, 'auth'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string,
|
||||
|
@ -66,7 +83,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
tcp_port = config.get(CONF_TCP_PORT)
|
||||
encryption = config.get(CONF_SSL)
|
||||
encryption = get_deprecated(config, CONF_PROXY_SSL, CONF_SSL)
|
||||
websocket = config.get(CONF_ENABLE_WEBSOCKET)
|
||||
|
||||
if host.startswith('http://') or host.startswith('https://'):
|
||||
|
@ -169,7 +186,6 @@ class KodiDevice(MediaPlayerDevice):
|
|||
self._properties = {}
|
||||
self._item = {}
|
||||
self._app_properties = {}
|
||||
self._ws_connected = False
|
||||
|
||||
@callback
|
||||
def async_on_speed_event(self, sender, data):
|
||||
|
@ -244,29 +260,26 @@ class KodiDevice(MediaPlayerDevice):
|
|||
"""Connect to Kodi via websocket protocol."""
|
||||
import jsonrpc_base
|
||||
try:
|
||||
yield from self._ws_server.ws_connect()
|
||||
ws_loop_future = yield from self._ws_server.ws_connect()
|
||||
except jsonrpc_base.jsonrpc.TransportError:
|
||||
_LOGGER.info("Unable to connect to Kodi via websocket")
|
||||
_LOGGER.debug(
|
||||
"Unable to connect to Kodi via websocket", exc_info=True)
|
||||
# Websocket connection is not required. Just return.
|
||||
return
|
||||
self.hass.loop.create_task(self.async_ws_loop())
|
||||
self._ws_connected = True
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_ws_loop(self):
|
||||
"""Run the websocket asyncio message loop."""
|
||||
import jsonrpc_base
|
||||
try:
|
||||
yield from self._ws_server.ws_loop()
|
||||
except jsonrpc_base.jsonrpc.TransportError:
|
||||
# Kodi abruptly ends ws connection when exiting. We only need to
|
||||
# know that it was closed.
|
||||
pass
|
||||
finally:
|
||||
yield from self._ws_server.close()
|
||||
self._ws_connected = False
|
||||
@asyncio.coroutine
|
||||
def ws_loop_wrapper():
|
||||
"""Catch exceptions from the websocket loop task."""
|
||||
try:
|
||||
yield from ws_loop_future
|
||||
except jsonrpc_base.TransportError:
|
||||
# Kodi abruptly ends ws connection when exiting. We will try
|
||||
# to reconnect on the next poll.
|
||||
pass
|
||||
|
||||
# Create a task instead of adding a tracking job, since this task will
|
||||
# run until the websocket connection is closed.
|
||||
self.hass.loop.create_task(ws_loop_wrapper())
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
|
@ -279,8 +292,8 @@ class KodiDevice(MediaPlayerDevice):
|
|||
self._app_properties = {}
|
||||
return
|
||||
|
||||
if self._enable_websocket and not self._ws_connected:
|
||||
self.hass.loop.create_task(self.async_ws_connect())
|
||||
if self._enable_websocket and not self._ws_server.connected:
|
||||
self.hass.async_add_job(self.async_ws_connect())
|
||||
|
||||
self._app_properties = \
|
||||
yield from self.server.Application.GetProperties(
|
||||
|
@ -299,7 +312,8 @@ class KodiDevice(MediaPlayerDevice):
|
|||
|
||||
self._item = (yield from self.server.Player.GetItem(
|
||||
player_id,
|
||||
['title', 'file', 'uniqueid', 'thumbnail', 'artist']
|
||||
['title', 'file', 'uniqueid', 'thumbnail', 'artist',
|
||||
'albumartist', 'showtitle', 'album', 'season', 'episode']
|
||||
))['item']
|
||||
else:
|
||||
self._properties = {}
|
||||
|
@ -309,7 +323,7 @@ class KodiDevice(MediaPlayerDevice):
|
|||
@property
|
||||
def server(self):
|
||||
"""Active server for json-rpc requests."""
|
||||
if self._ws_connected:
|
||||
if self._enable_websocket and self._ws_server.connected:
|
||||
return self._ws_server
|
||||
else:
|
||||
return self._http_server
|
||||
|
@ -322,7 +336,7 @@ class KodiDevice(MediaPlayerDevice):
|
|||
@property
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return not self._ws_connected
|
||||
return not (self._enable_websocket and self._ws_server.connected)
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
|
@ -343,8 +357,7 @@ class KodiDevice(MediaPlayerDevice):
|
|||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
if self._players is not None and len(self._players) > 0:
|
||||
return self._players[0]['type']
|
||||
return MEDIA_TYPES.get(self._item.get('type'))
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
|
@ -382,6 +395,44 @@ class KodiDevice(MediaPlayerDevice):
|
|||
return self._item.get(
|
||||
'title', self._item.get('label', self._item.get('file')))
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
"""Title of series of current playing media, TV show only."""
|
||||
return self._item.get('showtitle')
|
||||
|
||||
@property
|
||||
def media_season(self):
|
||||
"""Season of current playing media, TV show only."""
|
||||
return self._item.get('season')
|
||||
|
||||
@property
|
||||
def media_episode(self):
|
||||
"""Episode of current playing media, TV show only."""
|
||||
return self._item.get('episode')
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Album name of current playing media, music track only."""
|
||||
return self._item.get('album')
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media, music track only."""
|
||||
artists = self._item.get('artist', [])
|
||||
if len(artists) > 0:
|
||||
return artists[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_album_artist(self):
|
||||
"""Album artist of current playing media, music track only."""
|
||||
artists = self._item.get('albumartist', [])
|
||||
if len(artists) > 0:
|
||||
return artists[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag media player features that are supported."""
|
||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.components.media_player import (
|
|||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_PORT, CONF_PASSWORD,
|
||||
CONF_HOST)
|
||||
CONF_HOST, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
|
@ -25,9 +25,7 @@ REQUIREMENTS = ['python-mpd2==0.5.5']
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_LOCATION = 'location'
|
||||
|
||||
DEFAULT_LOCATION = 'MPD'
|
||||
DEFAULT_NAME = 'MPD'
|
||||
DEFAULT_PORT = 6600
|
||||
|
||||
PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=120)
|
||||
|
@ -38,7 +36,7 @@ SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
|
|||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
@ -49,9 +47,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
"""Setup the MPD platform."""
|
||||
daemon = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
location = config.get(CONF_LOCATION)
|
||||
name = config.get(CONF_NAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
import mpd
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
@ -75,20 +72,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
else:
|
||||
raise
|
||||
|
||||
add_devices([MpdDevice(daemon, port, location, password)])
|
||||
add_devices([MpdDevice(daemon, port, password, name)])
|
||||
|
||||
|
||||
class MpdDevice(MediaPlayerDevice):
|
||||
"""Representation of a MPD server."""
|
||||
|
||||
# pylint: disable=no-member
|
||||
def __init__(self, server, port, location, password):
|
||||
def __init__(self, server, port, password, name):
|
||||
"""Initialize the MPD device."""
|
||||
import mpd
|
||||
|
||||
self.server = server
|
||||
self.port = port
|
||||
self._name = location
|
||||
self._name = name
|
||||
self.password = password
|
||||
self.status = None
|
||||
self.currentsong = None
|
||||
|
|
|
@ -10,16 +10,35 @@ import os
|
|||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import homeassistant.util as util
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
from homeassistant import util
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_PLAY, MediaPlayerDevice)
|
||||
MEDIA_TYPE_MUSIC,
|
||||
MEDIA_TYPE_TVSHOW,
|
||||
MEDIA_TYPE_VIDEO,
|
||||
PLATFORM_SCHEMA,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
SUPPORT_PLAY,
|
||||
SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_STOP,
|
||||
SUPPORT_TURN_OFF,
|
||||
SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_VOLUME_SET,
|
||||
MediaPlayerDevice,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
||||
STATE_UNKNOWN)
|
||||
DEVICE_DEFAULT_NAME,
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers.event import (track_utc_time_change)
|
||||
|
||||
|
||||
REQUIREMENTS = ['plexapi==2.0.2']
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
@ -27,13 +46,24 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
|||
|
||||
PLEX_CONFIG_FILE = 'plex.conf'
|
||||
|
||||
CONF_INCLUDE_NON_CLIENTS = 'include_non_clients'
|
||||
CONF_USE_EPISODE_ART = 'use_episode_art'
|
||||
CONF_USE_CUSTOM_ENTITY_IDS = 'use_custom_entity_ids'
|
||||
CONF_SHOW_ALL_CONTROLS = 'show_all_controls'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_INCLUDE_NON_CLIENTS, default=False):
|
||||
cv.boolean,
|
||||
vol.Optional(CONF_USE_EPISODE_ART, default=False):
|
||||
cv.boolean,
|
||||
vol.Optional(CONF_USE_CUSTOM_ENTITY_IDS, default=False):
|
||||
cv.boolean,
|
||||
})
|
||||
|
||||
# Map ip to request id for configuring
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY
|
||||
|
||||
|
||||
def config_from_file(filename, config=None):
|
||||
"""Small configuration file management function."""
|
||||
|
@ -62,10 +92,12 @@ def config_from_file(filename, config=None):
|
|||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup the Plex platform."""
|
||||
config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
|
||||
if len(config):
|
||||
# get config from plex.conf
|
||||
file_config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
|
||||
|
||||
if len(file_config):
|
||||
# Setup a configured PlexServer
|
||||
host, token = config.popitem()
|
||||
host, token = file_config.popitem()
|
||||
token = token['token']
|
||||
# Via discovery
|
||||
elif discovery_info is not None:
|
||||
|
@ -79,22 +111,22 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
else:
|
||||
return
|
||||
|
||||
setup_plexserver(host, token, hass, add_devices_callback)
|
||||
setup_plexserver(host, token, hass, config, add_devices_callback)
|
||||
|
||||
|
||||
def setup_plexserver(host, token, hass, add_devices_callback):
|
||||
def setup_plexserver(host, token, hass, config, add_devices_callback):
|
||||
"""Setup a plexserver based on host parameter."""
|
||||
import plexapi.server
|
||||
import plexapi.exceptions
|
||||
|
||||
try:
|
||||
plexserver = plexapi.server.PlexServer('http://%s' % host, token)
|
||||
except (plexapi.exceptions.BadRequest,
|
||||
plexapi.exceptions.Unauthorized,
|
||||
except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized,
|
||||
plexapi.exceptions.NotFound) as error:
|
||||
_LOGGER.info(error)
|
||||
# No token or wrong token
|
||||
request_configuration(host, hass, add_devices_callback)
|
||||
request_configuration(host, hass, config,
|
||||
add_devices_callback)
|
||||
return
|
||||
|
||||
# If we came here and configuring this host, mark as done
|
||||
|
@ -106,8 +138,9 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
|||
|
||||
# Save config
|
||||
if not config_from_file(
|
||||
hass.config.path(PLEX_CONFIG_FILE),
|
||||
{host: {'token': token}}):
|
||||
hass.config.path(PLEX_CONFIG_FILE), {host: {
|
||||
'token': token
|
||||
}}):
|
||||
_LOGGER.error('failed to save config file')
|
||||
|
||||
_LOGGER.info('Connected to: http://%s', host)
|
||||
|
@ -117,6 +150,7 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
|||
track_utc_time_change(hass, lambda now: update_devices(), second=30)
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
# pylint: disable=too-many-branches
|
||||
def update_devices():
|
||||
"""Update the devices objects."""
|
||||
try:
|
||||
|
@ -125,8 +159,8 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
|||
_LOGGER.exception('Error listing plex devices')
|
||||
return
|
||||
except OSError:
|
||||
_LOGGER.error(
|
||||
'Could not connect to plex server at http://%s', host)
|
||||
_LOGGER.error('Could not connect to plex server at http://%s',
|
||||
host)
|
||||
return
|
||||
|
||||
new_plex_clients = []
|
||||
|
@ -136,12 +170,31 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
|||
continue
|
||||
|
||||
if device.machineIdentifier not in plex_clients:
|
||||
new_client = PlexClient(device, plex_sessions, update_devices,
|
||||
new_client = PlexClient(config, device, None,
|
||||
plex_sessions, update_devices,
|
||||
update_sessions)
|
||||
plex_clients[device.machineIdentifier] = new_client
|
||||
new_plex_clients.append(new_client)
|
||||
else:
|
||||
plex_clients[device.machineIdentifier].set_device(device)
|
||||
plex_clients[device.machineIdentifier].refresh(device, None)
|
||||
|
||||
# add devices with a session and no client (ex. PlexConnect Apple TV's)
|
||||
if config.get(CONF_INCLUDE_NON_CLIENTS):
|
||||
for machine_identifier, session in plex_sessions.items():
|
||||
if (machine_identifier not in plex_clients
|
||||
and machine_identifier is not None):
|
||||
new_client = PlexClient(config, None, session,
|
||||
plex_sessions, update_devices,
|
||||
update_sessions)
|
||||
plex_clients[machine_identifier] = new_client
|
||||
new_plex_clients.append(new_client)
|
||||
else:
|
||||
plex_clients[machine_identifier].refresh(None, session)
|
||||
|
||||
for machine_identifier, client in plex_clients.items():
|
||||
# force devices to idle that do not have a valid session
|
||||
if client.session is None:
|
||||
client.force_idle()
|
||||
|
||||
if new_plex_clients:
|
||||
add_devices_callback(new_plex_clients)
|
||||
|
@ -157,99 +210,333 @@ def setup_plexserver(host, token, hass, add_devices_callback):
|
|||
|
||||
plex_sessions.clear()
|
||||
for session in sessions:
|
||||
plex_sessions[session.player.machineIdentifier] = session
|
||||
if (session.player is not None and
|
||||
session.player.machineIdentifier is not None):
|
||||
plex_sessions[session.player.machineIdentifier] = session
|
||||
|
||||
update_devices()
|
||||
update_sessions()
|
||||
update_devices()
|
||||
|
||||
|
||||
def request_configuration(host, hass, add_devices_callback):
|
||||
def request_configuration(host, hass, config, add_devices_callback):
|
||||
"""Request configuration steps from the user."""
|
||||
configurator = get_component('configurator')
|
||||
|
||||
# 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.')
|
||||
configurator.notify_errors(_CONFIGURING[host],
|
||||
'Failed to register, please try again.')
|
||||
|
||||
return
|
||||
|
||||
def plex_configuration_callback(data):
|
||||
"""The actions to do when our configuration callback is called."""
|
||||
setup_plexserver(host, data.get('token'), hass, add_devices_callback)
|
||||
setup_plexserver(host,
|
||||
data.get('token'), hass, config,
|
||||
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'),
|
||||
entity_picture='/static/images/logo_plex_mediaserver.png',
|
||||
submit_caption='Confirm',
|
||||
fields=[{'id': 'token', 'name': 'X-Plex-Token', 'type': ''}]
|
||||
)
|
||||
fields=[{
|
||||
'id': 'token',
|
||||
'name': 'X-Plex-Token',
|
||||
'type': ''
|
||||
}])
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-public-methods
|
||||
class PlexClient(MediaPlayerDevice):
|
||||
"""Representation of a Plex device."""
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
def __init__(self, device, plex_sessions, update_devices, update_sessions):
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, config, device, session, plex_sessions,
|
||||
update_devices, update_sessions):
|
||||
"""Initialize the Plex device."""
|
||||
from plexapi.utils import NA
|
||||
|
||||
self._app_name = ''
|
||||
self._device = None
|
||||
self._device_protocol_capabilities = None
|
||||
self._is_player_active = False
|
||||
self._is_player_available = False
|
||||
self._machine_identifier = None
|
||||
self._make = ''
|
||||
self._media_content_id = None
|
||||
self._media_content_type = None
|
||||
self._media_duration = None
|
||||
self._media_image_url = None
|
||||
self._media_title = None
|
||||
self._name = None
|
||||
self._player_state = 'idle'
|
||||
self._previous_volume_level = 1 # Used in fake muting
|
||||
self._session = None
|
||||
self._session_type = None
|
||||
self._state = STATE_IDLE
|
||||
self._volume_level = 1 # since we can't retrieve remotely
|
||||
self._volume_muted = False # since we can't retrieve remotely
|
||||
self.na_type = NA
|
||||
self.config = config
|
||||
self.plex_sessions = plex_sessions
|
||||
self.update_devices = update_devices
|
||||
self.update_sessions = update_sessions
|
||||
self.set_device(device)
|
||||
self._season = None
|
||||
|
||||
def set_device(self, device):
|
||||
"""Set the device property."""
|
||||
self.device = device
|
||||
# Music
|
||||
self._media_album_artist = None
|
||||
self._media_album_name = None
|
||||
self._media_artist = None
|
||||
self._media_track = None
|
||||
|
||||
# TV Show
|
||||
self._media_episode = None
|
||||
self._media_season = None
|
||||
self._media_series_title = None
|
||||
|
||||
self.refresh(device, session)
|
||||
|
||||
# Assign custom entity ID if desired
|
||||
if self.config.get(CONF_USE_CUSTOM_ENTITY_IDS):
|
||||
prefix = ''
|
||||
# allow for namespace prefixing when using custom entity names
|
||||
if config.get("entity_namespace"):
|
||||
prefix = config.get("entity_namespace") + '_'
|
||||
|
||||
# rename the entity id
|
||||
if self.machine_identifier:
|
||||
self.entity_id = "%s.%s%s" % (
|
||||
'media_player', prefix,
|
||||
self.machine_identifier.lower().replace('-', '_'))
|
||||
else:
|
||||
if self.name:
|
||||
self.entity_id = "%s.%s%s" % (
|
||||
'media_player', prefix,
|
||||
self.name.lower().replace('-', '_'))
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-statements
|
||||
def refresh(self, device, session):
|
||||
"""Refresh key device data."""
|
||||
# new data refresh
|
||||
if session:
|
||||
self._session = session
|
||||
if device:
|
||||
self._device = device
|
||||
self._session = None
|
||||
|
||||
if self._device:
|
||||
self._machine_identifier = self._convert_na_to_none(
|
||||
self._device.machineIdentifier)
|
||||
self._name = self._convert_na_to_none(
|
||||
self._device.title) or DEVICE_DEFAULT_NAME
|
||||
self._device_protocol_capabilities = (
|
||||
self._device.protocolCapabilities)
|
||||
|
||||
# set valid session, preferring device session
|
||||
if self._device and self.plex_sessions.get(
|
||||
self._device.machineIdentifier, None):
|
||||
self._session = self._convert_na_to_none(self.plex_sessions.get(
|
||||
self._device.machineIdentifier, None))
|
||||
|
||||
if self._session:
|
||||
self._media_position = self._convert_na_to_none(
|
||||
self._session.viewOffset)
|
||||
self._media_content_id = self._convert_na_to_none(
|
||||
self._session.ratingKey)
|
||||
else:
|
||||
self._media_position = None
|
||||
self._media_content_id = None
|
||||
|
||||
# player dependent data
|
||||
if self._session and self._session.player:
|
||||
self._is_player_available = True
|
||||
self._machine_identifier = self._convert_na_to_none(
|
||||
self._session.player.machineIdentifier)
|
||||
self._name = self._convert_na_to_none(self._session.player.title)
|
||||
self._player_state = self._session.player.state
|
||||
self._make = self._convert_na_to_none(self._session.player.device)
|
||||
else:
|
||||
self._is_player_available = False
|
||||
|
||||
if self._player_state == 'playing':
|
||||
self._is_player_active = True
|
||||
self._state = STATE_PLAYING
|
||||
elif self._player_state == 'paused':
|
||||
self._is_player_active = True
|
||||
self._state = STATE_PAUSED
|
||||
elif self.device:
|
||||
self._is_player_active = False
|
||||
self._state = STATE_IDLE
|
||||
else:
|
||||
self._is_player_active = False
|
||||
self._state = STATE_OFF
|
||||
|
||||
if self._is_player_active and self._session is not None:
|
||||
self._session_type = self._session.type
|
||||
self._media_duration = self._convert_na_to_none(
|
||||
self._session.duration)
|
||||
else:
|
||||
self._session_type = None
|
||||
self._media_duration = None
|
||||
|
||||
# media type
|
||||
if self._session_type == 'clip':
|
||||
_LOGGER.debug('Clip content type detected, '
|
||||
'compatibility may vary: %s',
|
||||
self.entity_id)
|
||||
self._media_content_type = MEDIA_TYPE_TVSHOW
|
||||
elif self._session_type == 'episode':
|
||||
self._media_content_type = MEDIA_TYPE_TVSHOW
|
||||
elif self._session_type == 'movie':
|
||||
self._media_content_type = MEDIA_TYPE_VIDEO
|
||||
elif self._session_type == 'track':
|
||||
self._media_content_type = MEDIA_TYPE_MUSIC
|
||||
else:
|
||||
self._media_content_type = None
|
||||
|
||||
# title (movie name, tv episode name, music song name)
|
||||
if self._session:
|
||||
self._media_title = self._convert_na_to_none(self._session.title)
|
||||
|
||||
# Movies
|
||||
if (self.media_content_type == MEDIA_TYPE_VIDEO and
|
||||
self._convert_na_to_none(self._session.year) is not None):
|
||||
self._media_title += ' (' + str(self._session.year) + ')'
|
||||
|
||||
# TV Show
|
||||
if (self._is_player_active and
|
||||
self._media_content_type is MEDIA_TYPE_TVSHOW):
|
||||
|
||||
# season number (00)
|
||||
if callable(self._convert_na_to_none(self._session.seasons)):
|
||||
self._media_season = self._convert_na_to_none(
|
||||
self._session.seasons()[0].index).zfill(2)
|
||||
elif self._convert_na_to_none(
|
||||
self._session.parentIndex) is not None:
|
||||
self._media_season = self._session.parentIndex.zfill(2)
|
||||
else:
|
||||
self._media_season = None
|
||||
|
||||
# show name
|
||||
self._media_series_title = self._convert_na_to_none(
|
||||
self._session.grandparentTitle)
|
||||
|
||||
# episode number (00)
|
||||
if self._convert_na_to_none(
|
||||
self._session.index) is not None:
|
||||
self._media_episode = str(self._session.index).zfill(2)
|
||||
else:
|
||||
self._media_season = None
|
||||
self._media_series_title = None
|
||||
self._media_episode = None
|
||||
|
||||
# Music
|
||||
if (self._is_player_active and
|
||||
self._media_content_type == MEDIA_TYPE_MUSIC):
|
||||
self._media_album_name = self._convert_na_to_none(
|
||||
self._session.parentTitle)
|
||||
self._media_album_artist = self._convert_na_to_none(
|
||||
self._session.grandparentTitle)
|
||||
self._media_track = self._convert_na_to_none(self._session.index)
|
||||
self._media_artist = self._convert_na_to_none(
|
||||
self._session.originalTitle)
|
||||
# use album artist if track artist is missing
|
||||
if self._media_artist is None:
|
||||
_LOGGER.debug(
|
||||
'Using album artist because track artist '
|
||||
'was not found: %s', self.entity_id)
|
||||
self._media_artist = self._media_album_artist
|
||||
else:
|
||||
self._media_album_name = None
|
||||
self._media_album_artist = None
|
||||
self._media_track = None
|
||||
self._media_artist = None
|
||||
|
||||
# set app name to library name
|
||||
if (self._session is not None
|
||||
and self._session.librarySectionID is not None):
|
||||
self._app_name = self._convert_na_to_none(
|
||||
self._session.server.library.sectionByID(
|
||||
self._session.librarySectionID).title)
|
||||
else:
|
||||
self._app_name = ''
|
||||
|
||||
# media image url
|
||||
if self._session is not None:
|
||||
thumb_url = self._get_thumbnail_url(self._session.thumb)
|
||||
if (self.media_content_type is MEDIA_TYPE_TVSHOW
|
||||
and not self.config.get(CONF_USE_EPISODE_ART)):
|
||||
thumb_url = self._get_thumbnail_url(
|
||||
self._session.grandparentThumb)
|
||||
|
||||
if thumb_url is None:
|
||||
_LOGGER.debug('Using media art because media thumb '
|
||||
'was not found: %s', self.entity_id)
|
||||
thumb_url = self._get_thumbnail_url(self._session.art)
|
||||
|
||||
self._media_image_url = thumb_url
|
||||
else:
|
||||
self._media_image_url = None
|
||||
|
||||
def _get_thumbnail_url(self, property_value):
|
||||
"""Return full URL (if exists) for a thumbnail property."""
|
||||
if self._convert_na_to_none(property_value) is None:
|
||||
return None
|
||||
|
||||
if self._session is None or self._session.server is None:
|
||||
return None
|
||||
|
||||
url = self._session.server.url(property_value)
|
||||
response = requests.get(url, verify=False)
|
||||
if response and response.status_code == 200:
|
||||
return url
|
||||
|
||||
def force_idle(self):
|
||||
"""Force client to idle."""
|
||||
self._state = STATE_IDLE
|
||||
self._session = None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the id of this plex client."""
|
||||
return '{}.{}'.format(
|
||||
self.__class__, self.device.machineIdentifier or self.device.name)
|
||||
return '{}.{}'.format(self.__class__, self.machine_identifier or
|
||||
self.name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self.device.title or DEVICE_DEFAULT_NAME
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def machine_identifier(self):
|
||||
"""Return the machine identifier of the device."""
|
||||
return self._machine_identifier
|
||||
|
||||
@property
|
||||
def app_name(self):
|
||||
"""Return the library name of playing media."""
|
||||
return self._app_name
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
"""Return the device, if any."""
|
||||
return self._device
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
"""Return the session, if any."""
|
||||
return self.plex_sessions.get(self.device.machineIdentifier, None)
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self.session and self.session.player:
|
||||
state = self.session.player.state
|
||||
if state == 'playing':
|
||||
return STATE_PLAYING
|
||||
elif state == 'paused':
|
||||
return STATE_PAUSED
|
||||
# This is nasty. Need to find a way to determine alive
|
||||
elif self.device:
|
||||
return STATE_IDLE
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
return STATE_UNKNOWN
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Get the latest details."""
|
||||
from plexapi.video import Show
|
||||
|
||||
self.update_devices(no_throttle=True)
|
||||
self.update_sessions(no_throttle=True)
|
||||
|
||||
if isinstance(self.session, Show):
|
||||
self._season = self._convert_na_to_none(
|
||||
self.session.seasons()[0].index)
|
||||
|
||||
# pylint: disable=no-self-use, singleton-comparison
|
||||
def _convert_na_to_none(self, value):
|
||||
"""Convert PlexAPI _NA() instances to None."""
|
||||
|
@ -272,93 +559,298 @@ class PlexClient(MediaPlayerDevice):
|
|||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing media."""
|
||||
if self.session is not None:
|
||||
return self._convert_na_to_none(self.session.ratingKey)
|
||||
return self._media_content_id
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
if self.session is None:
|
||||
return None
|
||||
media_type = self.session.type
|
||||
if media_type == 'episode':
|
||||
if self._session_type == 'clip':
|
||||
_LOGGER.debug('Clip content type detected, '
|
||||
'compatibility may vary: %s',
|
||||
self.entity_id)
|
||||
return MEDIA_TYPE_TVSHOW
|
||||
elif media_type == 'movie':
|
||||
elif self._session_type == 'episode':
|
||||
return MEDIA_TYPE_TVSHOW
|
||||
elif self._session_type == 'movie':
|
||||
return MEDIA_TYPE_VIDEO
|
||||
elif media_type == 'track':
|
||||
elif self._session_type == 'track':
|
||||
return MEDIA_TYPE_MUSIC
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media, music track only."""
|
||||
return self._media_artist
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Album name of current playing media, music track only."""
|
||||
return self._media_album_name
|
||||
|
||||
@property
|
||||
def media_album_artist(self):
|
||||
"""Album artist of current playing media, music track only."""
|
||||
return self._media_album_artist
|
||||
|
||||
@property
|
||||
def media_track(self):
|
||||
"""Track number of current playing media, music track only."""
|
||||
return self._media_track
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
if self.session is not None:
|
||||
return self._convert_na_to_none(self.session.duration)
|
||||
return self._media_duration
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
if self.session is not None:
|
||||
thumb_url = self._convert_na_to_none(self.session.thumbUrl)
|
||||
if str(self.na_type) in thumb_url:
|
||||
# Audio tracks build their thumb urls internally before passing
|
||||
# back a URL with the PlexAPI _NA type already converted to a
|
||||
# string and embedded into a malformed URL
|
||||
thumb_url = None
|
||||
return thumb_url
|
||||
return self._media_image_url
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
# find a string we can use as a title
|
||||
if self.session is not None:
|
||||
return self._convert_na_to_none(self.session.title)
|
||||
return self._media_title
|
||||
|
||||
@property
|
||||
def media_season(self):
|
||||
"""Season of curent playing media (TV Show only)."""
|
||||
return self._season
|
||||
return self._media_season
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
"""The title of the series of current playing media (TV Show only)."""
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self._convert_na_to_none(self.session.grandparentTitle)
|
||||
return self._media_series_title
|
||||
|
||||
@property
|
||||
def media_episode(self):
|
||||
"""Episode of current playing media (TV Show only)."""
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self._convert_na_to_none(self.session.index)
|
||||
return self._media_episode
|
||||
|
||||
@property
|
||||
def make(self):
|
||||
"""The make of the device (ex. SHIELD Android TV)."""
|
||||
return self._make
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag media player features that are supported."""
|
||||
return SUPPORT_PLEX
|
||||
if not self._is_player_active:
|
||||
return None
|
||||
|
||||
# force show all controls
|
||||
if self.config.get(CONF_SHOW_ALL_CONTROLS):
|
||||
return (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK |
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_STOP |
|
||||
SUPPORT_VOLUME_SET | SUPPORT_PLAY |
|
||||
SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE)
|
||||
|
||||
# only show controls when we know what device is connecting
|
||||
if not self._make:
|
||||
return None
|
||||
# no mute support
|
||||
elif self.make.lower() == "shield android tv":
|
||||
_LOGGER.debug(
|
||||
'Shield Android TV client detected, disabling mute '
|
||||
'controls: %s', self.entity_id)
|
||||
return (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK |
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_STOP |
|
||||
SUPPORT_VOLUME_SET | SUPPORT_PLAY |
|
||||
SUPPORT_TURN_OFF)
|
||||
# Only supports play,pause,stop (and off which really is stop)
|
||||
elif self.make.lower().startswith("tivo"):
|
||||
_LOGGER.debug(
|
||||
'Tivo client detected, only enabling pause, play, '
|
||||
'stop, and off controls: %s', self.entity_id)
|
||||
return (SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP |
|
||||
SUPPORT_TURN_OFF)
|
||||
# Not all devices support playback functionality
|
||||
# Playback includes volume, stop/play/pause, etc.
|
||||
elif self.device and 'playback' in self._device_protocol_capabilities:
|
||||
return (SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK |
|
||||
SUPPORT_NEXT_TRACK | SUPPORT_STOP |
|
||||
SUPPORT_VOLUME_SET | SUPPORT_PLAY |
|
||||
SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE)
|
||||
else:
|
||||
return None
|
||||
|
||||
def _local_client_control_fix(self):
|
||||
"""Detect if local client and adjust url to allow control."""
|
||||
if self.device is None:
|
||||
return
|
||||
|
||||
# if this device's machineIdentifier matches an active client
|
||||
# with a loopback address, the device must be local or casting
|
||||
for client in self.device.server.clients():
|
||||
if ("127.0.0.1" in client.baseurl and
|
||||
client.machineIdentifier == self.device.machineIdentifier):
|
||||
# point controls to server since that's where the
|
||||
# playback is occuring
|
||||
_LOGGER.debug(
|
||||
'Local client detected, redirecting controls to '
|
||||
'Plex server: %s', self.entity_id)
|
||||
server_url = self.device.server.baseurl
|
||||
client_url = self.device.baseurl
|
||||
self.device.baseurl = "{}://{}:{}".format(
|
||||
urlparse(client_url).scheme,
|
||||
urlparse(server_url).hostname,
|
||||
str(urlparse(client_url).port))
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
self.device.setVolume(int(volume * 100),
|
||||
self._active_media_plexapi_type)
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
self._local_client_control_fix()
|
||||
self.device.setVolume(
|
||||
int(volume * 100), self._active_media_plexapi_type)
|
||||
self._volume_level = volume # store since we can't retrieve
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Return the volume level of the client (0..1)."""
|
||||
if (self._is_player_active and self.device and
|
||||
'playback' in self._device_protocol_capabilities):
|
||||
return self._volume_level
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Return boolean if volume is currently muted."""
|
||||
if self._is_player_active and self.device:
|
||||
return self._volume_muted
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Mute the volume.
|
||||
|
||||
Since we can't actually mute, we'll:
|
||||
- On mute, store volume and set volume to 0
|
||||
- On unmute, set volume to previously stored volume
|
||||
"""
|
||||
if not (self.device and
|
||||
'playback' in self._device_protocol_capabilities):
|
||||
return
|
||||
|
||||
self._volume_muted = mute
|
||||
if mute:
|
||||
self._previous_volume_level = self._volume_level
|
||||
self.set_volume_level(0)
|
||||
else:
|
||||
self.set_volume_level(self._previous_volume_level)
|
||||
|
||||
def media_play(self):
|
||||
"""Send play command."""
|
||||
self.device.play(self._active_media_plexapi_type)
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
self._local_client_control_fix()
|
||||
self.device.play(self._active_media_plexapi_type)
|
||||
|
||||
def media_pause(self):
|
||||
"""Send pause command."""
|
||||
self.device.pause(self._active_media_plexapi_type)
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
self._local_client_control_fix()
|
||||
self.device.pause(self._active_media_plexapi_type)
|
||||
|
||||
def media_stop(self):
|
||||
"""Send stop command."""
|
||||
self.device.stop(self._active_media_plexapi_type)
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
self._local_client_control_fix()
|
||||
self.device.stop(self._active_media_plexapi_type)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the client off."""
|
||||
# Fake it since we can't turn the client off
|
||||
self.media_stop()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self.device.skipNext(self._active_media_plexapi_type)
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
self._local_client_control_fix()
|
||||
self.device.skipNext(self._active_media_plexapi_type)
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send previous track command."""
|
||||
self.device.skipPrevious(self._active_media_plexapi_type)
|
||||
if self.device and 'playback' in self._device_protocol_capabilities:
|
||||
self._local_client_control_fix()
|
||||
self.device.skipPrevious(self._active_media_plexapi_type)
|
||||
|
||||
# pylint: disable=W0613
|
||||
def play_media(self, media_type, media_id, **kwargs):
|
||||
"""Play a piece of media."""
|
||||
if not (self.device and
|
||||
'playback' in self._device_protocol_capabilities):
|
||||
return
|
||||
|
||||
src = json.loads(media_id)
|
||||
|
||||
media = None
|
||||
if media_type == 'MUSIC':
|
||||
media = self.device.server.library.section(
|
||||
src['library_name']).get(src['artist_name']).album(
|
||||
src['album_name']).get(src['track_name'])
|
||||
elif media_type == 'EPISODE':
|
||||
media = self._get_episode(
|
||||
src['library_name'], src['show_name'],
|
||||
src['season_number'], src['episode_number'])
|
||||
elif media_type == 'PLAYLIST':
|
||||
media = self.device.server.playlist(src['playlist_name'])
|
||||
elif media_type == 'VIDEO':
|
||||
media = self.device.server.library.section(
|
||||
src['library_name']).get(src['video_name'])
|
||||
|
||||
if media:
|
||||
self._client_play_media(media, shuffle=src['shuffle'])
|
||||
|
||||
def _get_episode(self, library_name, show_name, season_number,
|
||||
episode_number):
|
||||
"""Find TV episode and return a Plex media object."""
|
||||
target_season = None
|
||||
target_episode = None
|
||||
|
||||
seasons = self.device.server.library.section(library_name).get(
|
||||
show_name).seasons()
|
||||
for season in seasons:
|
||||
if int(season.seasonNumber) == int(season_number):
|
||||
target_season = season
|
||||
break
|
||||
|
||||
if target_season is None:
|
||||
_LOGGER.error('Season not found: %s\\%s - S%sE%s', library_name,
|
||||
show_name,
|
||||
str(season_number).zfill(2),
|
||||
str(episode_number).zfill(2))
|
||||
else:
|
||||
for episode in target_season.episodes():
|
||||
if int(episode.index) == int(episode_number):
|
||||
target_episode = episode
|
||||
break
|
||||
|
||||
if target_episode is None:
|
||||
_LOGGER.error('Episode not found: %s\\%s - S%sE%s',
|
||||
library_name, show_name,
|
||||
str(season_number).zfill(2),
|
||||
str(episode_number).zfill(2))
|
||||
|
||||
return target_episode
|
||||
|
||||
def _client_play_media(self, media, **params):
|
||||
"""Instruct Plex client to play a piece of media."""
|
||||
if not (self.device and
|
||||
'playback' in self._device_protocol_capabilities):
|
||||
_LOGGER.error('Client cannot play media: %s', self.entity_id)
|
||||
return
|
||||
|
||||
import plexapi.playqueue
|
||||
server_url = media.server.baseurl.split(':')
|
||||
playqueue = plexapi.playqueue.PlayQueue.create(self.device.server,
|
||||
media, **params)
|
||||
self._local_client_control_fix()
|
||||
self.device.sendCommand('playback/playMedia', **dict({
|
||||
'machineIdentifier':
|
||||
self.device.server.machineIdentifier,
|
||||
'address':
|
||||
server_url[1].strip('/'),
|
||||
'port':
|
||||
server_url[-1],
|
||||
'key':
|
||||
media.key,
|
||||
'containerKey':
|
||||
'/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
|
||||
}, **params))
|
||||
|
|
237
homeassistant/components/media_player/volumio.py
Executable file
237
homeassistant/components/media_player/volumio.py
Executable file
|
@ -0,0 +1,237 @@
|
|||
"""
|
||||
Volumio Platform.
|
||||
|
||||
The volumio platform allows you to control a Volumio media player
|
||||
from Home Assistant.
|
||||
|
||||
|
||||
To add a Volumio player to your installation, add the following to
|
||||
your configuration.yaml file.
|
||||
|
||||
# Example configuration.yaml entry
|
||||
media_player:
|
||||
- platform: volumio
|
||||
name: 'Volumio Home Audio'
|
||||
host: homeaudio.local
|
||||
port: 3000
|
||||
Configuration variables:
|
||||
|
||||
- **name** (*Optional*): Name of the device
|
||||
- **host** (*Required*): IP address or hostname of the device
|
||||
- **port** (*Required*): Port number of Volumio service
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
||||
SUPPORT_PLAY, MediaPlayerDevice,
|
||||
PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC)
|
||||
from homeassistant.const import (
|
||||
STATE_PLAYING, STATE_PAUSED, STATE_IDLE, CONF_HOST, CONF_PORT, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
|
||||
_CONFIGURING = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'Volumio'
|
||||
DEFAULT_PORT = 3000
|
||||
TIMEOUT = 10
|
||||
|
||||
SUPPORT_VOLUMIO = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
|
||||
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup the Volumio platform."""
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
name = config.get(CONF_NAME)
|
||||
async_add_devices([Volumio(name, host, port, hass)])
|
||||
|
||||
|
||||
class Volumio(MediaPlayerDevice):
|
||||
"""Volumio Player Object."""
|
||||
|
||||
def __init__(self, name, host, port, hass):
|
||||
"""Initialize the media player."""
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.hass = hass
|
||||
self._url = host + ":" + str(port)
|
||||
self._name = name
|
||||
self._state = {}
|
||||
self.async_update()
|
||||
self._lastvol = self._state.get('volume', 0)
|
||||
|
||||
@asyncio.coroutine
|
||||
def send_volumio_msg(self, method, params=None):
|
||||
"""Send message."""
|
||||
url = "http://{}:{}/api/v1/{}/".format(
|
||||
self.host, self.port, method)
|
||||
response = None
|
||||
|
||||
_LOGGER.debug("URL: %s params: %s", url, params)
|
||||
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
response = yield from websession.get(url, params=params)
|
||||
if response.status == 200:
|
||||
data = yield from response.json()
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Query failed, response code: %s Full message: %s",
|
||||
response.status, response)
|
||||
return False
|
||||
|
||||
except (asyncio.TimeoutError,
|
||||
aiohttp.errors.ClientError,
|
||||
aiohttp.errors.ClientDisconnectedError) as error:
|
||||
_LOGGER.error("Failed communicating with Volumio: %s", type(error))
|
||||
return False
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
try:
|
||||
return data
|
||||
except AttributeError:
|
||||
_LOGGER.error("Received invalid response: %s", data)
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update state."""
|
||||
resp = yield from self.send_volumio_msg('getState')
|
||||
if resp is False:
|
||||
return
|
||||
self._state = resp.copy()
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
status = self._state.get('status', None)
|
||||
if status == 'pause':
|
||||
return STATE_PAUSED
|
||||
elif status == 'play':
|
||||
return STATE_PLAYING
|
||||
else:
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return self._state.get('title', None)
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media (Music track only)."""
|
||||
return self._state.get('artist', None)
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Artist of current playing media (Music track only)."""
|
||||
return self._state.get('album', None)
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
url = self._state.get('albumart', None)
|
||||
if url is None:
|
||||
return
|
||||
if str(url[0:2]).lower() == 'ht':
|
||||
mediaurl = url
|
||||
else:
|
||||
mediaurl = "http://" + self.host + ":" + str(self.port) + url
|
||||
return mediaurl
|
||||
|
||||
@property
|
||||
def media_seek_position(self):
|
||||
"""Time in seconds of current seek position."""
|
||||
return self._state.get('seek', None)
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Time in seconds of current song duration."""
|
||||
return self._state.get('duration', None)
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Volume level of the media player (0..1)."""
|
||||
volume = self._state.get('volume', None)
|
||||
if volume is not None:
|
||||
volume = volume / 100
|
||||
return volume
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Boolean if volume is currently muted."""
|
||||
return self._state.get('mute', None)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_VOLUMIO
|
||||
|
||||
def async_media_next_track(self):
|
||||
"""Send media_next command to media player."""
|
||||
return self.send_volumio_msg('commands', params={'cmd': 'next'})
|
||||
|
||||
def async_media_previous_track(self):
|
||||
"""Send media_previous command to media player."""
|
||||
return self.send_volumio_msg('commands', params={'cmd': 'prev'})
|
||||
|
||||
def async_media_play(self):
|
||||
"""Send media_play command to media player."""
|
||||
return self.send_volumio_msg('commands', params={'cmd': 'play'})
|
||||
|
||||
def async_media_pause(self):
|
||||
"""Send media_pause command to media player."""
|
||||
return self.send_volumio_msg('commands', params={'cmd': 'pause'})
|
||||
|
||||
def async_set_volume_level(self, volume):
|
||||
"""Send volume_up command to media player."""
|
||||
return self.send_volumio_msg('commands',
|
||||
params={'cmd': 'volume',
|
||||
'volume': int(volume * 100)})
|
||||
|
||||
def async_mute_volume(self, mute):
|
||||
"""Send mute command to media player."""
|
||||
mutecmd = 'mute' if mute else 'unmute'
|
||||
if mute:
|
||||
# mute is implemenhted as 0 volume, do save last volume level
|
||||
self._lastvol = self._state['volume']
|
||||
return self.send_volumio_msg('commands',
|
||||
params={'cmd': 'volume',
|
||||
'volume': mutecmd})
|
||||
else:
|
||||
return self.send_volumio_msg('commands',
|
||||
params={'cmd': 'volume',
|
||||
'volume': self._lastvol})
|
|
@ -9,6 +9,7 @@ import logging
|
|||
import os
|
||||
import socket
|
||||
import time
|
||||
import ssl
|
||||
import requests.certs
|
||||
|
||||
import voluptuous as vol
|
||||
|
@ -48,6 +49,7 @@ CONF_CERTIFICATE = 'certificate'
|
|||
CONF_CLIENT_KEY = 'client_key'
|
||||
CONF_CLIENT_CERT = 'client_cert'
|
||||
CONF_TLS_INSECURE = 'tls_insecure'
|
||||
CONF_TLS_VERSION = 'tls_version'
|
||||
|
||||
CONF_BIRTH_MESSAGE = 'birth_message'
|
||||
CONF_WILL_MESSAGE = 'will_message'
|
||||
|
@ -67,6 +69,7 @@ DEFAULT_RETAIN = False
|
|||
DEFAULT_PROTOCOL = PROTOCOL_311
|
||||
DEFAULT_DISCOVERY = False
|
||||
DEFAULT_DISCOVERY_PREFIX = 'homeassistant'
|
||||
DEFAULT_TLS_PROTOCOL = 'auto'
|
||||
|
||||
ATTR_TOPIC = 'topic'
|
||||
ATTR_PAYLOAD = 'payload'
|
||||
|
@ -116,12 +119,15 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_CERTIFICATE): cv.isfile,
|
||||
vol.Optional(CONF_CERTIFICATE): vol.Any('auto', cv.isfile),
|
||||
vol.Inclusive(CONF_CLIENT_KEY, 'client_key_auth',
|
||||
msg=CLIENT_KEY_AUTH_MSG): cv.isfile,
|
||||
vol.Inclusive(CONF_CLIENT_CERT, 'client_key_auth',
|
||||
msg=CLIENT_KEY_AUTH_MSG): cv.isfile,
|
||||
vol.Optional(CONF_TLS_INSECURE): cv.boolean,
|
||||
vol.Optional(CONF_TLS_VERSION,
|
||||
default=DEFAULT_TLS_PROTOCOL): vol.Any('auto', '1.0',
|
||||
'1.1', '1.2'),
|
||||
vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL):
|
||||
vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])),
|
||||
vol.Optional(CONF_EMBEDDED): HBMQTT_CONFIG_SCHEMA,
|
||||
|
@ -311,18 +317,34 @@ def async_setup(hass, config):
|
|||
certificate = os.path.join(os.path.dirname(__file__),
|
||||
'addtrustexternalcaroot.crt')
|
||||
|
||||
# When the port indicates mqtts, use bundled certificates from requests
|
||||
if certificate is None and port == 8883:
|
||||
# When the certificate is set to auto, use bundled certs from requests
|
||||
if certificate == 'auto':
|
||||
certificate = requests.certs.where()
|
||||
|
||||
will_message = conf.get(CONF_WILL_MESSAGE)
|
||||
birth_message = conf.get(CONF_BIRTH_MESSAGE)
|
||||
|
||||
# Be able to override versions other than TLSv1.0 under Python3.6
|
||||
conf_tls_version = conf.get(CONF_TLS_VERSION)
|
||||
if conf_tls_version == '1.2':
|
||||
tls_version = ssl.PROTOCOL_TLSv1_2
|
||||
elif conf_tls_version == '1.1':
|
||||
tls_version = ssl.PROTOCOL_TLSv1_1
|
||||
elif conf_tls_version == '1.0':
|
||||
tls_version = ssl.PROTOCOL_TLSv1
|
||||
else:
|
||||
import sys
|
||||
# Python3.6 supports automatic negotiation of highest TLS version
|
||||
if sys.hexversion >= 0x03060000:
|
||||
tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member
|
||||
else:
|
||||
tls_version = ssl.PROTOCOL_TLSv1
|
||||
|
||||
try:
|
||||
hass.data[DATA_MQTT] = MQTT(
|
||||
hass, broker, port, client_id, keepalive, username, password,
|
||||
certificate, client_key, client_cert, tls_insecure, protocol,
|
||||
will_message, birth_message)
|
||||
will_message, birth_message, tls_version)
|
||||
except socket.error:
|
||||
_LOGGER.exception("Can't connect to the broker. "
|
||||
"Please check your settings and the broker itself")
|
||||
|
@ -380,7 +402,8 @@ class MQTT(object):
|
|||
|
||||
def __init__(self, hass, broker, port, client_id, keepalive, username,
|
||||
password, certificate, client_key, client_cert,
|
||||
tls_insecure, protocol, will_message, birth_message):
|
||||
tls_insecure, protocol, will_message, birth_message,
|
||||
tls_version):
|
||||
"""Initialize Home Assistant MQTT client."""
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
|
@ -409,7 +432,8 @@ class MQTT(object):
|
|||
|
||||
if certificate is not None:
|
||||
self._mqttc.tls_set(
|
||||
certificate, certfile=client_cert, keyfile=client_key)
|
||||
certificate, certfile=client_cert,
|
||||
keyfile=client_key, tls_version=tls_version)
|
||||
|
||||
if tls_insecure is not None:
|
||||
self._mqttc.tls_insecure_set(tls_insecure)
|
||||
|
|
|
@ -84,6 +84,11 @@ class iOSNotificationService(BaseNotificationService):
|
|||
data[ATTR_DATA] = kwargs.get(ATTR_DATA)
|
||||
|
||||
for target in targets:
|
||||
if target not in ios.enabled_push_ids():
|
||||
_LOGGER.error("The target (%s) does not exist in ios.conf.",
|
||||
targets)
|
||||
return
|
||||
|
||||
data[ATTR_TARGET] = target
|
||||
|
||||
req = requests.post(PUSH_URL, json=data, timeout=10)
|
||||
|
|
|
@ -4,24 +4,33 @@ Kodi notification service.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.kodi/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (ATTR_ICON, CONF_HOST, CONF_PORT,
|
||||
CONF_USERNAME, CONF_PASSWORD)
|
||||
from homeassistant.components.notify import (ATTR_TITLE, ATTR_TITLE_DEFAULT,
|
||||
ATTR_DATA, PLATFORM_SCHEMA,
|
||||
BaseNotificationService)
|
||||
from homeassistant.const import (
|
||||
ATTR_ICON, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_PROXY_SSL)
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA,
|
||||
BaseNotificationService)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['jsonrpc-async==0.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['jsonrpc-requests==0.3']
|
||||
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_PROXY_SSL = False
|
||||
DEFAULT_TIMEOUT = 5
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_PROXY_SSL, default=DEFAULT_PROXY_SSL): cv.boolean,
|
||||
vol.Inclusive(CONF_USERNAME, 'auth'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string,
|
||||
})
|
||||
|
@ -29,51 +38,66 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
ATTR_DISPLAYTIME = 'displaytime'
|
||||
|
||||
|
||||
def get_service(hass, config, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_get_service(hass, config, discovery_info=None):
|
||||
"""Return the notify service."""
|
||||
url = '{}:{}'.format(config.get(CONF_HOST), config.get(CONF_PORT))
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
encryption = config.get(CONF_PROXY_SSL)
|
||||
|
||||
if host.startswith('http://') or host.startswith('https://'):
|
||||
host = host.lstrip('http://').lstrip('https://')
|
||||
_LOGGER.warning(
|
||||
"Kodi host name should no longer conatin http:// See updated "
|
||||
"definitions here: "
|
||||
"https://home-assistant.io/components/media_player.kodi/")
|
||||
|
||||
http_protocol = 'https' if encryption else 'http'
|
||||
url = '{}://{}:{}/jsonrpc'.format(http_protocol, host, port)
|
||||
|
||||
if username is not None:
|
||||
auth = (username, password)
|
||||
auth = aiohttp.BasicAuth(username, password)
|
||||
else:
|
||||
auth = None
|
||||
|
||||
return KODINotificationService(
|
||||
url,
|
||||
auth
|
||||
)
|
||||
return KodiNotificationService(hass, url, auth)
|
||||
|
||||
|
||||
class KODINotificationService(BaseNotificationService):
|
||||
class KodiNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for Kodi."""
|
||||
|
||||
def __init__(self, url, auth=None):
|
||||
def __init__(self, hass, url, auth=None):
|
||||
"""Initialize the service."""
|
||||
import jsonrpc_requests
|
||||
import jsonrpc_async
|
||||
self._url = url
|
||||
|
||||
kwargs = {'timeout': 5}
|
||||
kwargs = {
|
||||
'timeout': DEFAULT_TIMEOUT,
|
||||
'session': async_get_clientsession(hass),
|
||||
}
|
||||
|
||||
if auth is not None:
|
||||
kwargs['auth'] = auth
|
||||
|
||||
self._server = jsonrpc_requests.Server(
|
||||
'{}/jsonrpc'.format(self._url), **kwargs)
|
||||
self._server = jsonrpc_async.Server(self._url, **kwargs)
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_send_message(self, message="", **kwargs):
|
||||
"""Send a message to Kodi."""
|
||||
import jsonrpc_requests
|
||||
import jsonrpc_async
|
||||
try:
|
||||
data = kwargs.get(ATTR_DATA) or {}
|
||||
|
||||
displaytime = data.get(ATTR_DISPLAYTIME, 10000)
|
||||
icon = data.get(ATTR_ICON, "info")
|
||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
self._server.GUI.ShowNotification(title, message, icon,
|
||||
displaytime)
|
||||
yield from self._server.GUI.ShowNotification(
|
||||
title, message, icon, displaytime)
|
||||
|
||||
except jsonrpc_requests.jsonrpc.TransportError:
|
||||
except jsonrpc_async.TransportError:
|
||||
_LOGGER.warning('Unable to fetch Kodi data, Is Kodi online?')
|
||||
|
|
|
@ -18,7 +18,8 @@ from homeassistant.components.notify import (
|
|||
ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA, PLATFORM_SCHEMA,
|
||||
BaseNotificationService)
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SENDER, CONF_RECIPIENT)
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_TIMEOUT,
|
||||
CONF_SENDER, CONF_RECIPIENT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
|
@ -32,6 +33,7 @@ CONF_SERVER = 'server'
|
|||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 25
|
||||
DEFAULT_TIMEOUT = 5
|
||||
DEFAULT_DEBUG = False
|
||||
DEFAULT_STARTTLS = False
|
||||
|
||||
|
@ -40,6 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Required(CONF_RECIPIENT): vol.Email(),
|
||||
vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_SENDER): vol.Email(),
|
||||
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
|
@ -53,6 +56,7 @@ def get_service(hass, config, discovery_info=None):
|
|||
mail_service = MailNotificationService(
|
||||
config.get(CONF_SERVER),
|
||||
config.get(CONF_PORT),
|
||||
config.get(CONF_TIMEOUT),
|
||||
config.get(CONF_SENDER),
|
||||
config.get(CONF_STARTTLS),
|
||||
config.get(CONF_USERNAME),
|
||||
|
@ -69,11 +73,12 @@ def get_service(hass, config, discovery_info=None):
|
|||
class MailNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for E-Mail messages."""
|
||||
|
||||
def __init__(self, server, port, sender, starttls, username,
|
||||
def __init__(self, server, port, timeout, sender, starttls, username,
|
||||
password, recipient, debug):
|
||||
"""Initialize the service."""
|
||||
self._server = server
|
||||
self._port = port
|
||||
self._timeout = timeout
|
||||
self._sender = sender
|
||||
self.starttls = starttls
|
||||
self.username = username
|
||||
|
@ -84,7 +89,7 @@ class MailNotificationService(BaseNotificationService):
|
|||
|
||||
def connect(self):
|
||||
"""Connect/authenticate to SMTP Server."""
|
||||
mail = smtplib.SMTP(self._server, self._port, timeout=5)
|
||||
mail = smtplib.SMTP(self._server, self._port, timeout=self._timeout)
|
||||
mail.set_debuglevel(self.debug)
|
||||
mail.ehlo_or_helo_if_needed()
|
||||
if self.starttls:
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT
|
|||
|
||||
REQUIREMENTS = ['sleekxmpp==1.3.1',
|
||||
'dnspython3==1.15.0',
|
||||
'pyasn1==0.2.2',
|
||||
'pyasn1==0.2.3',
|
||||
'pyasn1-modules==0.0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
|
|
@ -35,7 +35,7 @@ from .util import session_scope
|
|||
|
||||
DOMAIN = 'recorder'
|
||||
|
||||
REQUIREMENTS = ['sqlalchemy==1.1.5']
|
||||
REQUIREMENTS = ['sqlalchemy==1.1.6']
|
||||
|
||||
DEFAULT_URL = 'sqlite:///{hass_config_path}'
|
||||
DEFAULT_DB_FILE = 'home-assistant_v2.db'
|
||||
|
@ -287,13 +287,27 @@ class Recorder(threading.Thread):
|
|||
|
||||
def _setup_connection(self):
|
||||
"""Ensure database is ready to fly."""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy import create_engine, event
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from . import models
|
||||
|
||||
kwargs = {}
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
@event.listens_for(Engine, "connect")
|
||||
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||
"""Set sqlite's WAL mode."""
|
||||
if self.db_url.startswith("sqlite://"):
|
||||
old_isolation = dbapi_connection.isolation_level
|
||||
dbapi_connection.isolation_level = None
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA journal_mode=WAL")
|
||||
cursor.close()
|
||||
dbapi_connection.isolation_level = old_isolation
|
||||
|
||||
if self.db_url == 'sqlite://' or ':memory:' in self.db_url:
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ def async_setup(hass, config):
|
|||
if not remote.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
update_coro = hass.async_add_job(
|
||||
remote.async_update_ha_state(True))
|
||||
if hasattr(remote, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
|
|
|
@ -90,7 +90,7 @@ def async_setup(hass, config):
|
|||
auth=auth
|
||||
)
|
||||
|
||||
if request.status == 200:
|
||||
if request.status < 400:
|
||||
_LOGGER.info("Success call %s.", request.url)
|
||||
return
|
||||
|
||||
|
|
|
@ -316,6 +316,12 @@ class RflinkCommand(RflinkDevice):
|
|||
cmd = str(int(args[0] / 17))
|
||||
self._state = True
|
||||
|
||||
elif command == 'toggle':
|
||||
cmd = 'on'
|
||||
# if the state is unknown or false, it gets set as true
|
||||
# if the state is true, it gets set as false
|
||||
self._state = self._state in [STATE_UNKNOWN, False]
|
||||
|
||||
# Send initial command and queue repetitions.
|
||||
# This allows the entity state to be updated quickly and not having to
|
||||
# wait for all repetitions to be sent
|
||||
|
@ -357,7 +363,7 @@ class RflinkCommand(RflinkDevice):
|
|||
self._protocol.send_command, self._device_id, cmd))
|
||||
|
||||
if repetitions > 1:
|
||||
self._repetition_task = self.hass.loop.create_task(
|
||||
self._repetition_task = self.hass.async_add_job(
|
||||
self._async_send_command(cmd, repetitions - 1))
|
||||
|
||||
|
||||
|
|
40
homeassistant/components/scene/wink.py
Normal file
40
homeassistant/components/scene/wink.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
"""
|
||||
Support for Wink scenes.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/scene.wink/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.scene import Scene
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Wink platform."""
|
||||
import pywink
|
||||
|
||||
for scene in pywink.get_scenes():
|
||||
_id = scene.object_id() + scene.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkScene(scene, hass)])
|
||||
|
||||
|
||||
class WinkScene(WinkDevice, Scene):
|
||||
"""Representation of a Wink shortcut/scene."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink device."""
|
||||
super().__init__(wink, hass)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Python-wink will always return False."""
|
||||
return self.wink.state()
|
||||
|
||||
def activate(self, **kwargs):
|
||||
"""Activate the scene."""
|
||||
self.wink.activate()
|
|
@ -99,11 +99,13 @@ class ComedHourlyPricingSensor(Entity):
|
|||
if self.type == CONF_FIVE_MINUTE:
|
||||
url_string = _RESOURCE + '?type=5minutefeed'
|
||||
response = get(url_string, timeout=10)
|
||||
self._state = float(response.json()[0]['price']) + self.offset
|
||||
self._state = round(
|
||||
float(response.json()[0]['price']) + self.offset, 2)
|
||||
elif self.type == CONF_CURRENT_HOUR_AVERAGE:
|
||||
url_string = _RESOURCE + '?type=currenthouraverage'
|
||||
response = get(url_string, timeout=10)
|
||||
self._state = float(response.json()[0]['price']) + self.offset
|
||||
self._state = round(
|
||||
float(response.json()[0]['price']) + self.offset, 2)
|
||||
else:
|
||||
self._state = STATE_UNKNOWN
|
||||
except (RequestException, ValueError, KeyError):
|
||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.const import CONF_NAME
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['py-cpuinfo==0.2.6']
|
||||
REQUIREMENTS = ['py-cpuinfo==0.2.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the CPU speed sensor."""
|
||||
"""Set up the CPU speed sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
add_devices([CpuSpeedSensor(name)])
|
||||
|
|
|
@ -13,7 +13,8 @@ from requests.exceptions import ConnectionError as ConnectError, \
|
|||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_NAME, CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION)
|
||||
CONF_API_KEY, CONF_NAME, CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION,
|
||||
CONF_LATITUDE, CONF_LONGITUDE)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -117,6 +118,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']),
|
||||
vol.Inclusive(CONF_LATITUDE, 'coordinates',
|
||||
'Latitude and longitude must exist together'): cv.latitude,
|
||||
vol.Inclusive(CONF_LONGITUDE, 'coordinates',
|
||||
'Latitude and longitude must exist together'): cv.longitude,
|
||||
vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=120)): (
|
||||
vol.All(cv.time_period, cv.positive_timedelta)),
|
||||
vol.Optional(CONF_FORECAST):
|
||||
|
@ -126,10 +131,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Dark Sky sensor."""
|
||||
# Validate the configuration
|
||||
if None in (hass.config.latitude, hass.config.longitude):
|
||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||
return False
|
||||
# latitude and longitude are inclusive on config
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
|
||||
if CONF_UNITS in config:
|
||||
units = config[CONF_UNITS]
|
||||
|
@ -140,8 +144,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
forecast_data = DarkSkyData(
|
||||
api_key=config.get(CONF_API_KEY, None),
|
||||
latitude=hass.config.latitude,
|
||||
longitude=hass.config.longitude,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
units=units,
|
||||
interval=config.get(CONF_UPDATE_INTERVAL))
|
||||
forecast_data.update()
|
||||
|
|
|
@ -108,7 +108,9 @@ class Dovado:
|
|||
"""Update device state."""
|
||||
_LOGGER.info("Updating")
|
||||
try:
|
||||
self.state.update(self._dovado.state or {})
|
||||
self.state = self._dovado.state or {}
|
||||
if not self.state:
|
||||
return False
|
||||
self.state.update(
|
||||
connected=self.state.get("modem status") == "CONNECTED")
|
||||
_LOGGER.debug("Received: %s", self.state)
|
||||
|
|
|
@ -40,7 +40,7 @@ import voluptuous as vol
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['dsmr_parser==0.6']
|
||||
REQUIREMENTS = ['dsmr_parser==0.8']
|
||||
|
||||
CONF_DSMR_VERSION = 'dsmr_version'
|
||||
CONF_RECONNECT_INTERVAL = 'reconnect_interval'
|
||||
|
@ -72,7 +72,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
logging.getLogger('dsmr_parser').setLevel(logging.ERROR)
|
||||
|
||||
from dsmr_parser import obis_references as obis_ref
|
||||
from dsmr_parser.protocol import create_dsmr_reader, create_tcp_dsmr_reader
|
||||
from dsmr_parser.clients.protocol import (create_dsmr_reader,
|
||||
create_tcp_dsmr_reader)
|
||||
import serial
|
||||
|
||||
dsmr_version = config[CONF_DSMR_VERSION]
|
||||
|
@ -110,7 +111,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
# Make all device entities aware of new telegram
|
||||
for device in devices:
|
||||
device.telegram = telegram
|
||||
hass.async_add_job(device.async_update_ha_state)
|
||||
hass.async_add_job(device.async_update_ha_state())
|
||||
|
||||
# Creates a asyncio.Protocol factory for reading DSMR telegrams from serial
|
||||
# and calls update_entities_telegram to update entities on arrival
|
||||
|
@ -132,7 +133,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
def connect_and_reconnect():
|
||||
"""Connect to DSMR and keep reconnecting until HA stops."""
|
||||
while hass.state != CoreState.stopping:
|
||||
# Start DSMR asycnio.Protocol reader
|
||||
# Start DSMR asyncio.Protocol reader
|
||||
try:
|
||||
transport, protocol = yield from hass.loop.create_task(
|
||||
reader_factory())
|
||||
|
@ -160,6 +161,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
yield from asyncio.sleep(config[CONF_RECONNECT_INTERVAL],
|
||||
loop=hass.loop)
|
||||
|
||||
# Cannot be hass.async_add_job because job runs forever
|
||||
hass.loop.create_task(connect_and_reconnect())
|
||||
|
||||
|
||||
|
|
|
@ -28,12 +28,14 @@ CONF_INSTANT = 'instant_readings'
|
|||
CONF_AMOUNT = 'amount'
|
||||
CONF_BUDGET = 'budget'
|
||||
CONF_COST = 'cost'
|
||||
CONF_CURRENT_VALUES = 'current_values'
|
||||
|
||||
SENSOR_TYPES = {
|
||||
CONF_INSTANT: ['Energy Usage', 'kW'],
|
||||
CONF_AMOUNT: ['Energy Consumed', 'kWh'],
|
||||
CONF_BUDGET: ['Energy Budget', None],
|
||||
CONF_COST: ['Energy Cost', None],
|
||||
CONF_CURRENT_VALUES: ['Per-Device Usage', 'kW']
|
||||
}
|
||||
|
||||
TYPES_SCHEMA = vol.In(SENSOR_TYPES)
|
||||
|
@ -57,19 +59,33 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
utc_offset = str(config.get(CONF_UTC_OFFSET))
|
||||
dev = []
|
||||
for variable in config[CONF_MONITORED_VARIABLES]:
|
||||
if variable[CONF_SENSOR_TYPE] == CONF_CURRENT_VALUES:
|
||||
url_string = _RESOURCE + 'getCurrentValuesSummary?token=' \
|
||||
+ app_token
|
||||
response = get(url_string, timeout=10)
|
||||
for sensor in response.json():
|
||||
sid = sensor['sid']
|
||||
dev.append(EfergySensor(variable[CONF_SENSOR_TYPE], app_token,
|
||||
utc_offset, variable[CONF_PERIOD],
|
||||
variable[CONF_CURRENCY], sid))
|
||||
dev.append(EfergySensor(
|
||||
variable[CONF_SENSOR_TYPE], app_token, utc_offset,
|
||||
variable[CONF_PERIOD], variable[CONF_CURRENCY]))
|
||||
|
||||
add_devices(dev)
|
||||
add_devices(dev, True)
|
||||
|
||||
|
||||
class EfergySensor(Entity):
|
||||
"""Implementation of an Efergy sensor."""
|
||||
|
||||
def __init__(self, sensor_type, app_token, utc_offset, period, currency):
|
||||
def __init__(self, sensor_type, app_token, utc_offset, period,
|
||||
currency, sid=None):
|
||||
"""Initialize the sensor."""
|
||||
self._name = SENSOR_TYPES[sensor_type][0]
|
||||
self.sid = sid
|
||||
if sid:
|
||||
self._name = 'efergy_' + sid
|
||||
else:
|
||||
self._name = SENSOR_TYPES[sensor_type][0]
|
||||
self.type = sensor_type
|
||||
self.app_token = app_token
|
||||
self.utc_offset = utc_offset
|
||||
|
@ -119,6 +135,14 @@ class EfergySensor(Entity):
|
|||
+ self.period
|
||||
response = get(url_string, timeout=10)
|
||||
self._state = response.json()['sum']
|
||||
elif self.type == 'current_values':
|
||||
url_string = _RESOURCE + 'getCurrentValuesSummary?token=' \
|
||||
+ self.app_token
|
||||
response = get(url_string, timeout=10)
|
||||
for sensor in response.json():
|
||||
if self.sid == sensor['sid']:
|
||||
measurement = next(iter(sensor['data'][0].values()))
|
||||
self._state = measurement / 1000
|
||||
else:
|
||||
self._state = 'Unknown'
|
||||
except (RequestException, ValueError, KeyError):
|
||||
|
|
|
@ -9,13 +9,19 @@ import socket
|
|||
import threading
|
||||
import datetime
|
||||
import time
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME)
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME,
|
||||
CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_NAME = 'Phone'
|
||||
|
@ -27,13 +33,24 @@ VALUE_RING = 'ringing'
|
|||
VALUE_CALL = 'dialing'
|
||||
VALUE_CONNECT = 'talking'
|
||||
VALUE_DISCONNECT = 'idle'
|
||||
CONF_PHONEBOOK = 'phonebook'
|
||||
CONF_PREFIXES = 'prefixes'
|
||||
|
||||
INTERVAL_RECONNECT = 60
|
||||
|
||||
# Return cached results if phonebook was downloaded less then this time ago.
|
||||
MIN_TIME_PHONEBOOK_UPDATE = datetime.timedelta(hours=6)
|
||||
SCAN_INTERVAL = datetime.timedelta(hours=3)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_PASSWORD, default='admin'): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=''): cv.string,
|
||||
vol.Optional(CONF_PHONEBOOK, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_PREFIXES, default=[]): vol.All(cv.ensure_list,
|
||||
[cv.string])
|
||||
})
|
||||
|
||||
|
||||
|
@ -42,14 +59,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
phonebook_id = config.get('phonebook')
|
||||
prefixes = config.get('prefixes')
|
||||
|
||||
sensor = FritzBoxCallSensor(name=name)
|
||||
try:
|
||||
phonebook = FritzBoxPhonebook(host=host, port=port,
|
||||
username=username, password=password,
|
||||
phonebook_id=phonebook_id,
|
||||
prefixes=prefixes)
|
||||
# pylint: disable=bare-except
|
||||
except:
|
||||
phonebook = None
|
||||
_LOGGER.warning('Phonebook with ID %s not found on Fritz!Box',
|
||||
phonebook_id)
|
||||
|
||||
sensor = FritzBoxCallSensor(name=name, phonebook=phonebook)
|
||||
|
||||
add_devices([sensor])
|
||||
|
||||
monitor = FritzBoxCallMonitor(host=host, port=port, sensor=sensor)
|
||||
monitor.connect()
|
||||
|
||||
def _stop_listener(_event):
|
||||
monitor.stopped.set()
|
||||
|
||||
hass.bus.listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
_stop_listener
|
||||
)
|
||||
|
||||
if monitor.sock is None:
|
||||
return False
|
||||
else:
|
||||
|
@ -59,11 +99,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class FritzBoxCallSensor(Entity):
|
||||
"""Implementation of a Fritz!Box call monitor."""
|
||||
|
||||
def __init__(self, name):
|
||||
def __init__(self, name, phonebook):
|
||||
"""Initialize the sensor."""
|
||||
self._state = VALUE_DEFAULT
|
||||
self._attributes = {}
|
||||
self._name = name
|
||||
self.phonebook = phonebook
|
||||
|
||||
def set_state(self, state):
|
||||
"""Set the state."""
|
||||
|
@ -75,8 +116,11 @@ class FritzBoxCallSensor(Entity):
|
|||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
"""Polling needed only to update phonebook, if defined."""
|
||||
if self.phonebook is None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
@ -93,6 +137,18 @@ class FritzBoxCallSensor(Entity):
|
|||
"""Return the state attributes."""
|
||||
return self._attributes
|
||||
|
||||
def number_to_name(self, number):
|
||||
"""Return a name for a given phone number."""
|
||||
if self.phonebook is None:
|
||||
return 'unknown'
|
||||
else:
|
||||
return self.phonebook.get_name(number)
|
||||
|
||||
def update(self):
|
||||
"""Update the phonebook if it is defined."""
|
||||
if self.phonebook is not None:
|
||||
self.phonebook.update_phonebook()
|
||||
|
||||
|
||||
class FritzBoxCallMonitor(object):
|
||||
"""Event listener to monitor calls on the Fritz!Box."""
|
||||
|
@ -103,6 +159,7 @@ class FritzBoxCallMonitor(object):
|
|||
self.port = port
|
||||
self.sock = None
|
||||
self._sensor = sensor
|
||||
self.stopped = threading.Event()
|
||||
|
||||
def connect(self):
|
||||
"""Connect to the Fritz!Box."""
|
||||
|
@ -110,7 +167,7 @@ class FritzBoxCallMonitor(object):
|
|||
self.sock.settimeout(10)
|
||||
try:
|
||||
self.sock.connect((self.host, self.port))
|
||||
threading.Thread(target=self._listen, daemon=True).start()
|
||||
threading.Thread(target=self._listen).start()
|
||||
except socket.error as err:
|
||||
self.sock = None
|
||||
_LOGGER.error("Cannot connect to %s on port %s: %s",
|
||||
|
@ -118,7 +175,7 @@ class FritzBoxCallMonitor(object):
|
|||
|
||||
def _listen(self):
|
||||
"""Listen to incoming or outgoing calls."""
|
||||
while True:
|
||||
while not self.stopped.isSet():
|
||||
try:
|
||||
response = self.sock.recv(2048)
|
||||
except socket.timeout:
|
||||
|
@ -152,6 +209,7 @@ class FritzBoxCallMonitor(object):
|
|||
"to": line[4],
|
||||
"device": line[5],
|
||||
"initiated": isotime}
|
||||
att["from_name"] = self._sensor.number_to_name(att["from"])
|
||||
self._sensor.set_attributes(att)
|
||||
elif line[1] == "CALL":
|
||||
self._sensor.set_state(VALUE_CALL)
|
||||
|
@ -160,13 +218,73 @@ class FritzBoxCallMonitor(object):
|
|||
"to": line[5],
|
||||
"device": line[6],
|
||||
"initiated": isotime}
|
||||
att["to_name"] = self._sensor.number_to_name(att["to"])
|
||||
self._sensor.set_attributes(att)
|
||||
elif line[1] == "CONNECT":
|
||||
self._sensor.set_state(VALUE_CONNECT)
|
||||
att = {"with": line[4], "device": [3], "accepted": isotime}
|
||||
att["with_name"] = self._sensor.number_to_name(att["with"])
|
||||
self._sensor.set_attributes(att)
|
||||
elif line[1] == "DISCONNECT":
|
||||
self._sensor.set_state(VALUE_DISCONNECT)
|
||||
att = {"duration": line[3], "closed": isotime}
|
||||
self._sensor.set_attributes(att)
|
||||
self._sensor.schedule_update_ha_state()
|
||||
|
||||
|
||||
class FritzBoxPhonebook(object):
|
||||
"""This connects to a FritzBox router and downloads its phone book."""
|
||||
|
||||
def __init__(self, host, port, username, password,
|
||||
phonebook_id=0, prefixes=None):
|
||||
"""Initialize the class."""
|
||||
self.host = host
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.port = port
|
||||
self.phonebook_id = phonebook_id
|
||||
self.phonebook_dict = None
|
||||
self.number_dict = None
|
||||
self.prefixes = prefixes or []
|
||||
|
||||
# pylint: disable=import-error
|
||||
import fritzconnection as fc
|
||||
# Establish a connection to the FRITZ!Box.
|
||||
self.fph = fc.FritzPhonebook(address=self.host,
|
||||
user=self.username,
|
||||
password=self.password)
|
||||
|
||||
if self.phonebook_id not in self.fph.list_phonebooks:
|
||||
raise ValueError("Phonebook with this ID not found.")
|
||||
|
||||
self.update_phonebook()
|
||||
|
||||
@Throttle(MIN_TIME_PHONEBOOK_UPDATE)
|
||||
def update_phonebook(self):
|
||||
"""Update the phone book dictionary."""
|
||||
self.phonebook_dict = self.fph.get_all_names(self.phonebook_id)
|
||||
self.number_dict = {re.sub(r'[^\d\+]', '', nr): name
|
||||
for name, nrs in self.phonebook_dict.items()
|
||||
for nr in nrs}
|
||||
_LOGGER.info('Fritz!Box phone book successfully updated.')
|
||||
|
||||
def get_name(self, number):
|
||||
"""Return a name for a given phone number."""
|
||||
number = re.sub(r'[^\d\+]', '', str(number))
|
||||
if self.number_dict is None:
|
||||
return 'unknown'
|
||||
try:
|
||||
return self.number_dict[number]
|
||||
except KeyError:
|
||||
pass
|
||||
if self.prefixes:
|
||||
for prefix in self.prefixes:
|
||||
try:
|
||||
return self.number_dict[prefix + number]
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
return self.number_dict[prefix + number.lstrip('0')]
|
||||
except KeyError:
|
||||
pass
|
||||
return 'unknown'
|
||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.util import Throttle
|
|||
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
REQUIREMENTS = ['fritzconnection==0.6']
|
||||
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv
|
|||
import homeassistant.helpers.location as location
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['googlemaps==2.4.4']
|
||||
REQUIREMENTS = ['googlemaps==2.4.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@ import homeassistant.helpers.config_validation as cv
|
|||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_ENTITY_ID, CONF_STATE, EVENT_HOMEASSISTANT_START)
|
||||
CONF_NAME, CONF_ENTITY_ID, CONF_STATE, CONF_TYPE,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
|
@ -31,15 +32,22 @@ CONF_END = 'end'
|
|||
CONF_DURATION = 'duration'
|
||||
CONF_PERIOD_KEYS = [CONF_START, CONF_END, CONF_DURATION]
|
||||
|
||||
CONF_TYPE_TIME = 'time'
|
||||
CONF_TYPE_RATIO = 'ratio'
|
||||
CONF_TYPE_COUNT = 'count'
|
||||
CONF_TYPE_KEYS = [CONF_TYPE_TIME, CONF_TYPE_RATIO, CONF_TYPE_COUNT]
|
||||
|
||||
DEFAULT_NAME = 'unnamed statistics'
|
||||
UNIT = 'h'
|
||||
UNIT_RATIO = '%'
|
||||
UNITS = {
|
||||
CONF_TYPE_TIME: 'h',
|
||||
CONF_TYPE_RATIO: '%',
|
||||
CONF_TYPE_COUNT: ''
|
||||
}
|
||||
ICON = 'mdi:chart-line'
|
||||
|
||||
ATTR_START = 'from'
|
||||
ATTR_END = 'to'
|
||||
ATTR_VALUE = 'value'
|
||||
ATTR_RATIO = 'ratio'
|
||||
|
||||
|
||||
def exactly_two_period_keys(conf):
|
||||
|
@ -62,6 +70,7 @@ PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({
|
|||
vol.Optional(CONF_START, default=None): cv.template,
|
||||
vol.Optional(CONF_END, default=None): cv.template,
|
||||
vol.Optional(CONF_DURATION, default=None): cv.time_period,
|
||||
vol.Optional(CONF_TYPE, default=CONF_TYPE_TIME): vol.In(CONF_TYPE_KEYS),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
}), exactly_two_period_keys)
|
||||
|
||||
|
@ -74,14 +83,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
start = config.get(CONF_START)
|
||||
end = config.get(CONF_END)
|
||||
duration = config.get(CONF_DURATION)
|
||||
sensor_type = config.get(CONF_TYPE)
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
for template in [start, end]:
|
||||
if template is not None:
|
||||
template.hass = hass
|
||||
|
||||
add_devices([HistoryStatsSensor(
|
||||
hass, entity_id, entity_state, start, end, duration, name)])
|
||||
add_devices([HistoryStatsSensor(hass, entity_id, entity_state, start, end,
|
||||
duration, sensor_type, name)])
|
||||
|
||||
return True
|
||||
|
||||
|
@ -90,7 +100,8 @@ class HistoryStatsSensor(Entity):
|
|||
"""Representation of a HistoryStats sensor."""
|
||||
|
||||
def __init__(
|
||||
self, hass, entity_id, entity_state, start, end, duration, name):
|
||||
self, hass, entity_id, entity_state, start, end, duration,
|
||||
sensor_type, name):
|
||||
"""Initialize the HistoryStats sensor."""
|
||||
self._hass = hass
|
||||
|
||||
|
@ -99,11 +110,13 @@ class HistoryStatsSensor(Entity):
|
|||
self._duration = duration
|
||||
self._start = start
|
||||
self._end = end
|
||||
self._type = sensor_type
|
||||
self._name = name
|
||||
self._unit_of_measurement = UNIT
|
||||
self._unit_of_measurement = UNITS[sensor_type]
|
||||
|
||||
self._period = (datetime.datetime.now(), datetime.datetime.now())
|
||||
self.value = 0
|
||||
self.count = 0
|
||||
|
||||
def force_refresh(*args):
|
||||
"""Force the component to refresh."""
|
||||
|
@ -123,7 +136,14 @@ class HistoryStatsSensor(Entity):
|
|||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return round(self.value, 2)
|
||||
if self._type == CONF_TYPE_TIME:
|
||||
return round(self.value, 2)
|
||||
|
||||
if self._type == CONF_TYPE_RATIO:
|
||||
return HistoryStatsHelper.pretty_ratio(self.value, self._period)
|
||||
|
||||
if self._type == CONF_TYPE_COUNT:
|
||||
return self.count
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
|
@ -142,7 +162,6 @@ class HistoryStatsSensor(Entity):
|
|||
hsh = HistoryStatsHelper
|
||||
return {
|
||||
ATTR_VALUE: hsh.pretty_duration(self.value),
|
||||
ATTR_RATIO: hsh.pretty_ratio(self.value, self._period),
|
||||
ATTR_START: start.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
ATTR_END: end.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
}
|
||||
|
@ -175,6 +194,7 @@ class HistoryStatsSensor(Entity):
|
|||
last_state == self._entity_state)
|
||||
last_time = dt_util.as_timestamp(start)
|
||||
elapsed = 0
|
||||
count = 0
|
||||
|
||||
# Make calculations
|
||||
for item in history_list.get(self._entity_id):
|
||||
|
@ -183,6 +203,8 @@ class HistoryStatsSensor(Entity):
|
|||
|
||||
if last_state:
|
||||
elapsed += current_time - last_time
|
||||
if current_state and not last_state:
|
||||
count += 1
|
||||
|
||||
last_state = current_state
|
||||
last_time = current_time
|
||||
|
@ -196,6 +218,9 @@ class HistoryStatsSensor(Entity):
|
|||
# Save value in hours
|
||||
self.value = elapsed / 3600
|
||||
|
||||
# Save counter
|
||||
self.count = count
|
||||
|
||||
def update_period(self):
|
||||
"""Parse the templates and store a datetime tuple in _period."""
|
||||
start = None
|
||||
|
@ -267,10 +292,10 @@ class HistoryStatsHelper:
|
|||
def pretty_ratio(value, period):
|
||||
"""Format the ratio of value / period duration."""
|
||||
if len(period) != 2 or period[0] == period[1]:
|
||||
return '0,0' + UNIT_RATIO
|
||||
return 0.0
|
||||
|
||||
ratio = 100 * 3600 * value / (period[1] - period[0]).total_seconds()
|
||||
return str(round(ratio, 1)) + UNIT_RATIO
|
||||
return round(ratio, 1)
|
||||
|
||||
@staticmethod
|
||||
def handle_template_exception(ex, field):
|
||||
|
|
234
homeassistant/components/sensor/lyft.py
Normal file
234
homeassistant/components/sensor/lyft.py
Normal file
|
@ -0,0 +1,234 @@
|
|||
"""
|
||||
Support for the Lyft API.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.lyft/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['lyft_rides==0.1.0b0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_CLIENT_SECRET = 'client_secret'
|
||||
CONF_END_LATITUDE = 'end_latitude'
|
||||
CONF_END_LONGITUDE = 'end_longitude'
|
||||
CONF_PRODUCT_IDS = 'product_ids'
|
||||
CONF_START_LATITUDE = 'start_latitude'
|
||||
CONF_START_LONGITUDE = 'start_longitude'
|
||||
|
||||
ICON = 'mdi:taxi'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||
vol.Required(CONF_START_LATITUDE): cv.latitude,
|
||||
vol.Required(CONF_START_LONGITUDE): cv.longitude,
|
||||
vol.Optional(CONF_END_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_END_LONGITUDE): cv.longitude,
|
||||
vol.Optional(CONF_PRODUCT_IDS, default=None):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Lyft sensor."""
|
||||
from lyft_rides.auth import ClientCredentialGrant
|
||||
|
||||
auth_flow = ClientCredentialGrant(client_id=config.get(CONF_CLIENT_ID),
|
||||
client_secret=config.get(
|
||||
CONF_CLIENT_SECRET),
|
||||
scopes="public",
|
||||
is_sandbox_mode=False)
|
||||
session = auth_flow.get_session()
|
||||
|
||||
wanted_product_ids = config.get(CONF_PRODUCT_IDS)
|
||||
|
||||
dev = []
|
||||
timeandpriceest = LyftEstimate(
|
||||
session, config[CONF_START_LATITUDE], config[CONF_START_LONGITUDE],
|
||||
config.get(CONF_END_LATITUDE), config.get(CONF_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(LyftSensor('time', timeandpriceest, product_id, product))
|
||||
if product.get('estimate') is not None:
|
||||
dev.append(LyftSensor(
|
||||
'price', timeandpriceest, product_id, product))
|
||||
add_devices(dev, True)
|
||||
|
||||
|
||||
class LyftSensor(Entity):
|
||||
"""Implementation of an Lyft sensor."""
|
||||
|
||||
def __init__(self, sensorType, products, product_id, product):
|
||||
"""Initialize the Lyft sensor."""
|
||||
self.data = products
|
||||
self._product_id = product_id
|
||||
self._product = product
|
||||
self._sensortype = sensorType
|
||||
self._name = '{} {}'.format(self._product['display_name'],
|
||||
self._sensortype)
|
||||
if 'lyft' not in self._name.lower():
|
||||
self._name = 'Lyft{}'.format(self._name)
|
||||
if self._sensortype == 'time':
|
||||
self._unit_of_measurement = 'min'
|
||||
elif self._sensortype == 'price':
|
||||
estimate = self._product['estimate']
|
||||
if estimate is not None:
|
||||
self._unit_of_measurement = estimate.get('currency')
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
params = {
|
||||
'Product ID': self._product['ride_type'],
|
||||
'Product display name': self._product['display_name'],
|
||||
'Vehicle Capacity': self._product['seats']
|
||||
}
|
||||
|
||||
if self._product.get('pricing_details') is not None:
|
||||
pricing_details = self._product['pricing_details']
|
||||
params['Base price'] = pricing_details.get('base_charge')
|
||||
params['Cancellation fee'] = pricing_details.get(
|
||||
'cancel_penalty_amount')
|
||||
params['Minimum price'] = pricing_details.get('cost_minimum')
|
||||
params['Cost per mile'] = pricing_details.get('cost_per_mile')
|
||||
params['Cost per minute'] = pricing_details.get('cost_per_minute')
|
||||
params['Price currency code'] = pricing_details.get('currency')
|
||||
params['Service fee'] = pricing_details.get('trust_and_service')
|
||||
|
||||
if self._product.get("estimate") is not None:
|
||||
estimate = self._product['estimate']
|
||||
params['Trip distance (in miles)'] = estimate.get(
|
||||
'estimated_distance_miles')
|
||||
params['High price estimate (in cents)'] = estimate.get(
|
||||
'estimated_cost_cents_max')
|
||||
params['Low price estimate (in cents)'] = estimate.get(
|
||||
'estimated_cost_cents_min')
|
||||
params['Trip duration (in seconds)'] = estimate.get(
|
||||
'estimated_duration_seconds')
|
||||
|
||||
# Ignore the Prime Time percentage -- the Lyft API always
|
||||
# returns 0 unless a user is logged in.
|
||||
# params['Prime Time percentage'] = estimate.get(
|
||||
# 'primetime_percentage')
|
||||
|
||||
if self._product.get("eta") is not None:
|
||||
eta = self._product['eta']
|
||||
params['Pickup time estimate (in seconds)'] = eta.get(
|
||||
'eta_seconds')
|
||||
|
||||
return {k: v for k, v in params.items() if v is not None}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return ICON
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from the Lyft API and update the states."""
|
||||
self.data.update()
|
||||
try:
|
||||
self._product = self.data.products[self._product_id]
|
||||
except KeyError:
|
||||
return
|
||||
if self._sensortype == 'time':
|
||||
eta = self._product['eta']
|
||||
if (eta is not None) and (eta.get('is_valid_estimate')):
|
||||
time_estimate = eta.get('eta_seconds', 0)
|
||||
self._state = int(time_estimate / 60)
|
||||
else:
|
||||
self._state = 0
|
||||
elif self._sensortype == 'price':
|
||||
estimate = self._product['estimate']
|
||||
if (estimate is not None) and \
|
||||
estimate.get('is_valid_estimate'):
|
||||
self._state = (int(
|
||||
(estimate.get('estimated_cost_cents_min', 0) +
|
||||
estimate.get('estimated_cost_cents_max', 0)) / 2) / 100)
|
||||
else:
|
||||
self._state = 0
|
||||
|
||||
|
||||
class LyftEstimate(object):
|
||||
"""The class for handling the time and price estimate."""
|
||||
|
||||
def __init__(self, session, start_latitude, start_longitude,
|
||||
end_latitude=None, end_longitude=None):
|
||||
"""Initialize the LyftEstimate object."""
|
||||
self._session = session
|
||||
self.start_latitude = start_latitude
|
||||
self.start_longitude = start_longitude
|
||||
self.end_latitude = end_latitude
|
||||
self.end_longitude = end_longitude
|
||||
self.products = None
|
||||
self.__real_update()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest product info and estimates from the Lyft API."""
|
||||
self.__real_update()
|
||||
|
||||
def __real_update(self):
|
||||
from lyft_rides.client import LyftRidesClient
|
||||
client = LyftRidesClient(self._session)
|
||||
|
||||
self.products = {}
|
||||
|
||||
products_response = client.get_ride_types(
|
||||
self.start_latitude, self.start_longitude)
|
||||
|
||||
products = products_response.json.get('ride_types')
|
||||
|
||||
for product in products:
|
||||
self.products[product['ride_type']] = product
|
||||
|
||||
if self.end_latitude is not None and self.end_longitude is not None:
|
||||
price_response = client.get_cost_estimates(
|
||||
self.start_latitude, self.start_longitude,
|
||||
self.end_latitude, self.end_longitude)
|
||||
|
||||
prices = price_response.json.get('cost_estimates', [])
|
||||
|
||||
for price in prices:
|
||||
product = self.products[price['ride_type']]
|
||||
if price.get("is_valid_estimate"):
|
||||
product['estimate'] = price
|
||||
|
||||
eta_response = client.get_pickup_time_estimates(
|
||||
self.start_latitude, self.start_longitude)
|
||||
|
||||
etas = eta_response.json.get('eta_estimates')
|
||||
|
||||
for eta in etas:
|
||||
if eta.get("is_valid_estimate"):
|
||||
self.products[eta['ride_type']]['eta'] = eta
|
|
@ -15,7 +15,7 @@ import homeassistant.util.dt as dt_util
|
|||
from homeassistant.helpers.entity import Entity
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['astral==1.3.4']
|
||||
REQUIREMENTS = ['astral==1.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -19,12 +19,16 @@ import homeassistant.helpers.config_validation as cv
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FORCE_UPDATE = 'force_update'
|
||||
|
||||
DEFAULT_NAME = 'MQTT Sensor'
|
||||
DEFAULT_FORCE_UPDATE = False
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
|
@ -43,6 +47,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_UNIT_OF_MEASUREMENT),
|
||||
config.get(CONF_FORCE_UPDATE),
|
||||
value_template,
|
||||
)])
|
||||
|
||||
|
@ -51,13 +56,14 @@ class MqttSensor(Entity):
|
|||
"""Representation of a sensor that can be updated using MQTT."""
|
||||
|
||||
def __init__(self, name, state_topic, qos, unit_of_measurement,
|
||||
value_template):
|
||||
force_update, value_template):
|
||||
"""Initialize the sensor."""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._qos = qos
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
self._force_update = force_update
|
||||
self._template = value_template
|
||||
|
||||
def async_added_to_hass(self):
|
||||
|
@ -92,6 +98,11 @@ class MqttSensor(Entity):
|
|||
"""Return the unit this state is expressed in."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def force_update(self):
|
||||
"""Force update."""
|
||||
return self._force_update
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
"""
|
||||
Support for monitoring an Neurio hub.
|
||||
Support for monitoring a Neurio energy sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.neurio_energy/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import requests.exceptions
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (CONF_API_KEY, CONF_NAME)
|
||||
from homeassistant.const import (CONF_API_KEY)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['neurio==0.3.1']
|
||||
|
||||
|
@ -21,48 +24,133 @@ _LOGGER = logging.getLogger(__name__)
|
|||
CONF_API_SECRET = 'api_secret'
|
||||
CONF_SENSOR_ID = 'sensor_id'
|
||||
|
||||
DEFAULT_NAME = 'Energy Usage'
|
||||
ACTIVE_NAME = 'Energy Usage'
|
||||
DAILY_NAME = 'Daily Energy Usage'
|
||||
|
||||
ACTIVE_TYPE = 'active'
|
||||
DAILY_TYPE = 'daily'
|
||||
|
||||
ICON = 'mdi:flash'
|
||||
|
||||
MIN_TIME_BETWEEN_DAILY_UPDATES = timedelta(seconds=150)
|
||||
MIN_TIME_BETWEEN_ACTIVE_UPDATES = timedelta(seconds=10)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_API_SECRET): cv.string,
|
||||
vol.Optional(CONF_SENSOR_ID): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Neurio sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
api_secret = config.get(CONF_API_SECRET)
|
||||
sensor_id = config.get(CONF_SENSOR_ID)
|
||||
|
||||
if not sensor_id:
|
||||
data = NeurioData(api_key, api_secret, sensor_id)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_DAILY_UPDATES)
|
||||
def update_daily():
|
||||
"""Update the daily power usage."""
|
||||
data.get_daily_usage()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_ACTIVE_UPDATES)
|
||||
def update_active():
|
||||
"""Update the active power usage."""
|
||||
data.get_active_power()
|
||||
|
||||
update_daily()
|
||||
update_active()
|
||||
|
||||
# Active power sensor
|
||||
add_devices([NeurioEnergy(data, ACTIVE_NAME, ACTIVE_TYPE, update_active)])
|
||||
# Daily power sensor
|
||||
add_devices([NeurioEnergy(data, DAILY_NAME, DAILY_TYPE, update_daily)])
|
||||
|
||||
|
||||
class NeurioData(object):
|
||||
"""Stores data retrieved from Neurio sensor."""
|
||||
|
||||
def __init__(self, api_key, api_secret, sensor_id):
|
||||
"""Initialize the data."""
|
||||
import neurio
|
||||
neurio_tp = neurio.TokenProvider(key=api_key, secret=api_secret)
|
||||
neurio_client = neurio.Client(token_provider=neurio_tp)
|
||||
user_info = neurio_client.get_user_information()
|
||||
_LOGGER.warning('Sensor ID auto-detected, set api_sensor_id: "%s"',
|
||||
user_info["locations"][0]["sensors"][0]["sensorId"])
|
||||
sensor_id = user_info["locations"][0]["sensors"][0]["sensorId"]
|
||||
|
||||
add_devices([NeurioEnergy(api_key, api_secret, name, sensor_id)])
|
||||
|
||||
|
||||
class NeurioEnergy(Entity):
|
||||
"""Implementation of an Neurio energy."""
|
||||
|
||||
def __init__(self, api_key, api_secret, name, sensor_id):
|
||||
"""Initialize the sensor."""
|
||||
self._name = name
|
||||
self.api_key = api_key
|
||||
self.api_secret = api_secret
|
||||
self.sensor_id = sensor_id
|
||||
|
||||
self._daily_usage = None
|
||||
self._active_power = None
|
||||
|
||||
self._state = None
|
||||
self._unit_of_measurement = 'W'
|
||||
|
||||
neurio_tp = neurio.TokenProvider(key=api_key, secret=api_secret)
|
||||
self.neurio_client = neurio.Client(token_provider=neurio_tp)
|
||||
|
||||
if not self.sensor_id:
|
||||
user_info = self.neurio_client.get_user_information()
|
||||
_LOGGER.warning('Sensor ID auto-detected: %s', user_info[
|
||||
"locations"][0]["sensors"][0]["sensorId"])
|
||||
self.sensor_id = user_info[
|
||||
"locations"][0]["sensors"][0]["sensorId"]
|
||||
|
||||
@property
|
||||
def daily_usage(self):
|
||||
"""Return latest daily usage value."""
|
||||
return self._daily_usage
|
||||
|
||||
@property
|
||||
def active_power(self):
|
||||
"""Return latest active power value."""
|
||||
return self._active_power
|
||||
|
||||
def get_active_power(self):
|
||||
"""Return current power value."""
|
||||
try:
|
||||
sample = self.neurio_client.get_samples_live_last(self.sensor_id)
|
||||
self._active_power = sample['consumptionPower']
|
||||
except (requests.exceptions.RequestException, ValueError, KeyError):
|
||||
_LOGGER.warning('Could not update current power usage.')
|
||||
return None
|
||||
|
||||
def get_daily_usage(self):
|
||||
"""Return current daily power usage."""
|
||||
kwh = 0
|
||||
start_time = dt_util.start_of_local_day() \
|
||||
.astimezone(dt_util.UTC).isoformat()
|
||||
end_time = dt_util.utcnow().isoformat()
|
||||
|
||||
_LOGGER.debug('Start: %s, End: %s', start_time, end_time)
|
||||
|
||||
try:
|
||||
history = self.neurio_client.get_samples_stats(
|
||||
self.sensor_id, start_time, 'days', end_time)
|
||||
except (requests.exceptions.RequestException, ValueError, KeyError):
|
||||
_LOGGER.warning('Could not update daily power usage.')
|
||||
return None
|
||||
|
||||
for result in history:
|
||||
kwh += result['consumptionEnergy'] / 3600000
|
||||
|
||||
self._daily_usage = round(kwh, 2)
|
||||
|
||||
|
||||
class NeurioEnergy(Entity):
|
||||
"""Implementation of a Neurio energy sensor."""
|
||||
|
||||
def __init__(self, data, name, sensor_type, update_call):
|
||||
"""Initialize the sensor."""
|
||||
self._name = name
|
||||
self._data = data
|
||||
self._sensor_type = sensor_type
|
||||
self.update_sensor = update_call
|
||||
self._state = None
|
||||
|
||||
if sensor_type == ACTIVE_TYPE:
|
||||
self._unit_of_measurement = 'W'
|
||||
elif sensor_type == DAILY_TYPE:
|
||||
self._unit_of_measurement = 'kWh'
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -85,14 +173,10 @@ class NeurioEnergy(Entity):
|
|||
return ICON
|
||||
|
||||
def update(self):
|
||||
"""Get the Neurio monitor data from the web service."""
|
||||
import neurio
|
||||
try:
|
||||
neurio_tp = neurio.TokenProvider(
|
||||
key=self.api_key, secret=self.api_secret)
|
||||
neurio_client = neurio.Client(token_provider=neurio_tp)
|
||||
sample = neurio_client.get_samples_live_last(
|
||||
sensor_id=self.sensor_id)
|
||||
self._state = sample['consumptionPower']
|
||||
except (requests.exceptions.RequestException, ValueError, KeyError):
|
||||
_LOGGER.warning('Could not update status for %s', self.name)
|
||||
"""Get the latest data, update state."""
|
||||
self.update_sensor()
|
||||
|
||||
if self._sensor_type == ACTIVE_TYPE:
|
||||
self._state = self._data.active_power
|
||||
elif self._sensor_type == DAILY_TYPE:
|
||||
self._state = self._data.daily_usage
|
||||
|
|
|
@ -127,6 +127,6 @@ class OctoPrintSensor(Entity):
|
|||
# Error calling the api, already logged in api.update()
|
||||
return
|
||||
|
||||
if self._state is None:
|
||||
if self._state is None and self.sensor_type != "completion":
|
||||
_LOGGER.warning("Unable to locate value for %s", self.sensor_type)
|
||||
return
|
||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['psutil==5.1.3']
|
||||
REQUIREMENTS = ['psutil==5.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
209
homeassistant/components/sensor/tado.py
Normal file
209
homeassistant/components/sensor/tado.py
Normal file
|
@ -0,0 +1,209 @@
|
|||
"""tado component to create some sensors for each zone."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.tado import (
|
||||
DATA_TADO)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES = ['temperature', 'humidity', 'power',
|
||||
'link', 'heating', 'tado mode', 'overlay']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the sensor platform."""
|
||||
# get the PyTado object from the hub component
|
||||
tado = hass.data[DATA_TADO]
|
||||
|
||||
try:
|
||||
zones = tado.get_zones()
|
||||
except RuntimeError:
|
||||
_LOGGER.error("Unable to get zone info from mytado")
|
||||
return False
|
||||
|
||||
sensor_items = []
|
||||
for zone in zones:
|
||||
if zone['type'] == 'HEATING':
|
||||
for variable in SENSOR_TYPES:
|
||||
sensor_items.append(create_zone_sensor(
|
||||
tado, zone, zone['name'], zone['id'],
|
||||
variable))
|
||||
|
||||
me_data = tado.get_me()
|
||||
sensor_items.append(create_device_sensor(
|
||||
tado, me_data, me_data['homes'][0]['name'],
|
||||
me_data['homes'][0]['id'], "tado bridge status"))
|
||||
|
||||
if len(sensor_items) > 0:
|
||||
add_devices(sensor_items, True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def create_zone_sensor(tado, zone, name, zone_id, variable):
|
||||
"""Create a zone sensor."""
|
||||
data_id = 'zone {} {}'.format(name, zone_id)
|
||||
|
||||
tado.add_sensor(data_id, {
|
||||
"zone": zone,
|
||||
"name": name,
|
||||
"id": zone_id,
|
||||
"data_id": data_id
|
||||
})
|
||||
|
||||
return TadoSensor(tado, name, zone_id, variable, data_id)
|
||||
|
||||
|
||||
def create_device_sensor(tado, device, name, device_id, variable):
|
||||
"""Create a device sensor."""
|
||||
data_id = 'device {} {}'.format(name, device_id)
|
||||
|
||||
tado.add_sensor(data_id, {
|
||||
"device": device,
|
||||
"name": name,
|
||||
"id": device_id,
|
||||
"data_id": data_id
|
||||
})
|
||||
|
||||
return TadoSensor(tado, name, device_id, variable, data_id)
|
||||
|
||||
|
||||
class TadoSensor(Entity):
|
||||
"""Representation of a tado Sensor."""
|
||||
|
||||
def __init__(self, store, zone_name, zone_id, zone_variable, data_id):
|
||||
"""Initialization of TadoSensor class."""
|
||||
self._store = store
|
||||
|
||||
self.zone_name = zone_name
|
||||
self.zone_id = zone_id
|
||||
self.zone_variable = zone_variable
|
||||
|
||||
self._unique_id = '{} {}'.format(zone_variable, zone_id)
|
||||
self._data_id = data_id
|
||||
|
||||
self._state = None
|
||||
self._state_attributes = None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return '{} {}'.format(self.zone_name, self.zone_variable)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return self._state_attributes
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
if self.zone_variable == "temperature":
|
||||
return self.hass.config.units.temperature_unit
|
||||
elif self.zone_variable == "humidity":
|
||||
return '%'
|
||||
elif self.zone_variable == "heating":
|
||||
return '%'
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon for the sensor."""
|
||||
if self.zone_variable == "temperature":
|
||||
return 'mdi:thermometer'
|
||||
elif self.zone_variable == "humidity":
|
||||
return 'mdi:water-percent'
|
||||
|
||||
def update(self):
|
||||
"""Update method called when should_poll is true."""
|
||||
self._store.update()
|
||||
|
||||
data = self._store.get_data(self._data_id)
|
||||
|
||||
if data is None:
|
||||
_LOGGER.debug('Recieved no data for zone %s',
|
||||
self.zone_name)
|
||||
return
|
||||
|
||||
unit = TEMP_CELSIUS
|
||||
|
||||
# pylint: disable=R0912
|
||||
if self.zone_variable == 'temperature':
|
||||
if 'sensorDataPoints' in data:
|
||||
sensor_data = data['sensorDataPoints']
|
||||
temperature = float(
|
||||
sensor_data['insideTemperature']['celsius'])
|
||||
|
||||
self._state = self.hass.config.units.temperature(
|
||||
temperature, unit)
|
||||
self._state_attributes = {
|
||||
"time":
|
||||
sensor_data['insideTemperature']['timestamp'],
|
||||
"setting": 0 # setting is used in climate device
|
||||
}
|
||||
|
||||
# temperature setting will not exist when device is off
|
||||
if 'temperature' in data['setting'] and \
|
||||
data['setting']['temperature'] is not None:
|
||||
temperature = float(
|
||||
data['setting']['temperature']['celsius'])
|
||||
|
||||
self._state_attributes["setting"] = \
|
||||
self.hass.config.units.temperature(
|
||||
temperature, unit)
|
||||
|
||||
elif self.zone_variable == 'humidity':
|
||||
if 'sensorDataPoints' in data:
|
||||
sensor_data = data['sensorDataPoints']
|
||||
self._state = float(
|
||||
sensor_data['humidity']['percentage'])
|
||||
self._state_attributes = {
|
||||
"time": sensor_data['humidity']['timestamp'],
|
||||
}
|
||||
|
||||
elif self.zone_variable == 'power':
|
||||
if 'setting' in data:
|
||||
self._state = data['setting']['power']
|
||||
|
||||
elif self.zone_variable == 'link':
|
||||
if 'link' in data:
|
||||
self._state = data['link']['state']
|
||||
|
||||
elif self.zone_variable == 'heating':
|
||||
if 'activityDataPoints' in data:
|
||||
activity_data = data['activityDataPoints']
|
||||
self._state = float(
|
||||
activity_data['heatingPower']['percentage'])
|
||||
self._state_attributes = {
|
||||
"time": activity_data['heatingPower']['timestamp'],
|
||||
}
|
||||
|
||||
elif self.zone_variable == 'tado bridge status':
|
||||
if 'connectionState' in data:
|
||||
self._state = data['connectionState']['value']
|
||||
|
||||
elif self.zone_variable == 'tado mode':
|
||||
if 'tadoMode' in data:
|
||||
self._state = data['tadoMode']
|
||||
|
||||
elif self.zone_variable == 'overlay':
|
||||
if 'overlay' in data and data['overlay'] is not None:
|
||||
self._state = True
|
||||
self._state_attributes = {
|
||||
"termination": data['overlay']['termination']['type'],
|
||||
}
|
||||
else:
|
||||
self._state = False
|
||||
self._state_attributes = {}
|
|
@ -41,11 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def decode(value):
|
||||
"""Double-decode required."""
|
||||
return value.encode('raw_unicode_escape').decode('utf-8')
|
||||
|
||||
|
||||
def convert_pid(value):
|
||||
"""Convert pid from hex string to integer."""
|
||||
return int(value, 16)
|
||||
|
@ -94,10 +89,10 @@ class TorqueReceiveDataView(HomeAssistantView):
|
|||
|
||||
if is_name:
|
||||
pid = convert_pid(is_name.group(1))
|
||||
names[pid] = decode(data[key])
|
||||
names[pid] = data[key]
|
||||
elif is_unit:
|
||||
pid = convert_pid(is_unit.group(1))
|
||||
units[pid] = decode(data[key])
|
||||
units[pid] = data[key]
|
||||
elif is_value:
|
||||
pid = convert_pid(is_value.group(1))
|
||||
if pid in self.sensors:
|
||||
|
@ -110,7 +105,7 @@ class TorqueReceiveDataView(HomeAssistantView):
|
|||
units.get(pid, None))
|
||||
hass.async_add_job(self.add_devices, [self.sensors[pid]])
|
||||
|
||||
return None
|
||||
return "OK!"
|
||||
|
||||
|
||||
class TorqueSensor(Entity):
|
||||
|
|
|
@ -15,34 +15,32 @@ from homeassistant.components.zwave import async_setup_platform # noqa # pylint
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_device(node, value, **kwargs):
|
||||
def get_device(node, values, **kwargs):
|
||||
"""Create zwave entity device."""
|
||||
# Generic Device mappings
|
||||
if value.command_class == zwave.const.COMMAND_CLASS_BATTERY:
|
||||
return ZWaveSensor(value)
|
||||
if node.has_command_class(zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL):
|
||||
return ZWaveMultilevelSensor(value)
|
||||
return ZWaveMultilevelSensor(values)
|
||||
if node.has_command_class(zwave.const.COMMAND_CLASS_METER) and \
|
||||
value.type == zwave.const.TYPE_DECIMAL:
|
||||
return ZWaveMultilevelSensor(value)
|
||||
values.primary.type == zwave.const.TYPE_DECIMAL:
|
||||
return ZWaveMultilevelSensor(values)
|
||||
if node.has_command_class(zwave.const.COMMAND_CLASS_ALARM) or \
|
||||
node.has_command_class(zwave.const.COMMAND_CLASS_SENSOR_ALARM):
|
||||
return ZWaveAlarmSensor(value)
|
||||
return ZWaveAlarmSensor(values)
|
||||
return None
|
||||
|
||||
|
||||
class ZWaveSensor(zwave.ZWaveDeviceEntity):
|
||||
"""Representation of a Z-Wave sensor."""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, values):
|
||||
"""Initialize the sensor."""
|
||||
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
|
||||
zwave.ZWaveDeviceEntity.__init__(self, values, DOMAIN)
|
||||
self.update_properties()
|
||||
|
||||
def update_properties(self):
|
||||
"""Callback on data changes for node values."""
|
||||
self._state = self._value.data
|
||||
self._units = self._value.units
|
||||
self._state = self.values.primary.data
|
||||
self._units = self.values.primary.units
|
||||
|
||||
@property
|
||||
def force_update(self):
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue