mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
commit
9bc16157af
12
.coveragerc
12
.coveragerc
@ -122,6 +122,8 @@ omit =
|
|||||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||||
homeassistant/components/binary_sensor/arest.py
|
homeassistant/components/binary_sensor/arest.py
|
||||||
homeassistant/components/binary_sensor/concord232.py
|
homeassistant/components/binary_sensor/concord232.py
|
||||||
|
homeassistant/components/binary_sensor/flic.py
|
||||||
|
homeassistant/components/binary_sensor/hikvision.py
|
||||||
homeassistant/components/binary_sensor/rest.py
|
homeassistant/components/binary_sensor/rest.py
|
||||||
homeassistant/components/browser.py
|
homeassistant/components/browser.py
|
||||||
homeassistant/components/camera/amcrest.py
|
homeassistant/components/camera/amcrest.py
|
||||||
@ -165,6 +167,7 @@ omit =
|
|||||||
homeassistant/components/discovery.py
|
homeassistant/components/discovery.py
|
||||||
homeassistant/components/downloader.py
|
homeassistant/components/downloader.py
|
||||||
homeassistant/components/emoncms_history.py
|
homeassistant/components/emoncms_history.py
|
||||||
|
homeassistant/components/emulated_hue/upnp.py
|
||||||
homeassistant/components/fan/mqtt.py
|
homeassistant/components/fan/mqtt.py
|
||||||
homeassistant/components/feedreader.py
|
homeassistant/components/feedreader.py
|
||||||
homeassistant/components/foursquare.py
|
homeassistant/components/foursquare.py
|
||||||
@ -183,6 +186,7 @@ omit =
|
|||||||
homeassistant/components/light/x10.py
|
homeassistant/components/light/x10.py
|
||||||
homeassistant/components/light/yeelight.py
|
homeassistant/components/light/yeelight.py
|
||||||
homeassistant/components/lirc.py
|
homeassistant/components/lirc.py
|
||||||
|
homeassistant/components/media_player/aquostv.py
|
||||||
homeassistant/components/media_player/braviatv.py
|
homeassistant/components/media_player/braviatv.py
|
||||||
homeassistant/components/media_player/cast.py
|
homeassistant/components/media_player/cast.py
|
||||||
homeassistant/components/media_player/cmus.py
|
homeassistant/components/media_player/cmus.py
|
||||||
@ -210,6 +214,7 @@ omit =
|
|||||||
homeassistant/components/media_player/snapcast.py
|
homeassistant/components/media_player/snapcast.py
|
||||||
homeassistant/components/media_player/sonos.py
|
homeassistant/components/media_player/sonos.py
|
||||||
homeassistant/components/media_player/squeezebox.py
|
homeassistant/components/media_player/squeezebox.py
|
||||||
|
homeassistant/components/media_player/vlc.py
|
||||||
homeassistant/components/media_player/yamaha.py
|
homeassistant/components/media_player/yamaha.py
|
||||||
homeassistant/components/notify/aws_lambda.py
|
homeassistant/components/notify/aws_lambda.py
|
||||||
homeassistant/components/notify/aws_sns.py
|
homeassistant/components/notify/aws_sns.py
|
||||||
@ -248,6 +253,7 @@ omit =
|
|||||||
homeassistant/components/sensor/bbox.py
|
homeassistant/components/sensor/bbox.py
|
||||||
homeassistant/components/sensor/bitcoin.py
|
homeassistant/components/sensor/bitcoin.py
|
||||||
homeassistant/components/sensor/bom.py
|
homeassistant/components/sensor/bom.py
|
||||||
|
homeassistant/components/sensor/broadlink.py
|
||||||
homeassistant/components/sensor/coinmarketcap.py
|
homeassistant/components/sensor/coinmarketcap.py
|
||||||
homeassistant/components/sensor/cpuspeed.py
|
homeassistant/components/sensor/cpuspeed.py
|
||||||
homeassistant/components/sensor/cups.py
|
homeassistant/components/sensor/cups.py
|
||||||
@ -280,6 +286,7 @@ omit =
|
|||||||
homeassistant/components/sensor/mhz19.py
|
homeassistant/components/sensor/mhz19.py
|
||||||
homeassistant/components/sensor/miflora.py
|
homeassistant/components/sensor/miflora.py
|
||||||
homeassistant/components/sensor/mqtt_room.py
|
homeassistant/components/sensor/mqtt_room.py
|
||||||
|
homeassistant/components/sensor/netdata.py
|
||||||
homeassistant/components/sensor/neurio_energy.py
|
homeassistant/components/sensor/neurio_energy.py
|
||||||
homeassistant/components/sensor/nut.py
|
homeassistant/components/sensor/nut.py
|
||||||
homeassistant/components/sensor/nzbget.py
|
homeassistant/components/sensor/nzbget.py
|
||||||
@ -292,6 +299,7 @@ omit =
|
|||||||
homeassistant/components/sensor/pvoutput.py
|
homeassistant/components/sensor/pvoutput.py
|
||||||
homeassistant/components/sensor/sabnzbd.py
|
homeassistant/components/sensor/sabnzbd.py
|
||||||
homeassistant/components/sensor/scrape.py
|
homeassistant/components/sensor/scrape.py
|
||||||
|
homeassistant/components/sensor/sensehat.py
|
||||||
homeassistant/components/sensor/serial_pm.py
|
homeassistant/components/sensor/serial_pm.py
|
||||||
homeassistant/components/sensor/snmp.py
|
homeassistant/components/sensor/snmp.py
|
||||||
homeassistant/components/sensor/sonarr.py
|
homeassistant/components/sensor/sonarr.py
|
||||||
@ -313,10 +321,12 @@ omit =
|
|||||||
homeassistant/components/sensor/waqi.py
|
homeassistant/components/sensor/waqi.py
|
||||||
homeassistant/components/sensor/xbox_live.py
|
homeassistant/components/sensor/xbox_live.py
|
||||||
homeassistant/components/sensor/yweather.py
|
homeassistant/components/sensor/yweather.py
|
||||||
homeassistant/components/sensor/waqi.py
|
homeassistant/components/sensor/zamg.py
|
||||||
homeassistant/components/switch/acer_projector.py
|
homeassistant/components/switch/acer_projector.py
|
||||||
homeassistant/components/switch/anel_pwrctrl.py
|
homeassistant/components/switch/anel_pwrctrl.py
|
||||||
homeassistant/components/switch/arest.py
|
homeassistant/components/switch/arest.py
|
||||||
|
homeassistant/components/switch/broadlink.py
|
||||||
|
homeassistant/components/switch/digitalloggers.py
|
||||||
homeassistant/components/switch/dlink.py
|
homeassistant/components/switch/dlink.py
|
||||||
homeassistant/components/switch/edimax.py
|
homeassistant/components/switch/edimax.py
|
||||||
homeassistant/components/switch/hikvisioncam.py
|
homeassistant/components/switch/hikvisioncam.py
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
# Contributing to Home Assistant
|
# Contributing to Home Assistant
|
||||||
|
|
||||||
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
|
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
|
||||||
|
|
||||||
The process is straight-forward.
|
The process is straight-forward.
|
||||||
|
|
||||||
|
- Read [How to get faster PR reviews](https://github.com/kubernetes/kubernetes/blob/master/docs/devel/faster_reviews.md) by Kubernetes (but skip step 0)
|
||||||
- Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
|
- Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
|
||||||
- Write the code for your device, notification service, sensor, or IoT thing.
|
- Write the code for your device, notification service, sensor, or IoT thing.
|
||||||
- Ensure tests work.
|
- Ensure tests work.
|
||||||
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
|
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
|
||||||
|
|
||||||
Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.
|
Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.
|
||||||
|
|
||||||
|
@ -8,9 +8,12 @@ WORKDIR /usr/src/app
|
|||||||
|
|
||||||
RUN pip3 install --no-cache-dir colorlog cython
|
RUN pip3 install --no-cache-dir colorlog cython
|
||||||
|
|
||||||
# For the nmap tracker, bluetooth tracker, Z-Wave
|
# For the nmap tracker, bluetooth tracker, Z-Wave, tellstick
|
||||||
RUN apt-get update && \
|
RUN echo "deb http://download.telldus.com/debian/ stable main" >> /etc/apt/sources.list.d/telldus.list && \
|
||||||
apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev && \
|
wget -qO - http://download.telldus.se/debian/telldus-public.key | apt-key add - && \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev \
|
||||||
|
libtelldus-core2 && \
|
||||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
COPY script/build_python_openzwave script/build_python_openzwave
|
COPY script/build_python_openzwave script/build_python_openzwave
|
||||||
|
@ -20,6 +20,7 @@ import homeassistant.loader as loader
|
|||||||
import homeassistant.util.package as pkg_util
|
import homeassistant.util.package as pkg_util
|
||||||
from homeassistant.util.async import (
|
from homeassistant.util.async import (
|
||||||
run_coroutine_threadsafe, run_callback_threadsafe)
|
run_coroutine_threadsafe, run_callback_threadsafe)
|
||||||
|
from homeassistant.util.logging import AsyncHandler
|
||||||
from homeassistant.util.yaml import clear_secret_cache
|
from homeassistant.util.yaml import clear_secret_cache
|
||||||
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
@ -528,6 +529,10 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# AsyncHandler allready exists?
|
||||||
|
if hass.data.get(core.DATA_ASYNCHANDLER):
|
||||||
|
return
|
||||||
|
|
||||||
# Log errors to a file if we have write access to file or config dir
|
# Log errors to a file if we have write access to file or config dir
|
||||||
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||||
err_path_exists = os.path.isfile(err_log_path)
|
err_path_exists = os.path.isfile(err_log_path)
|
||||||
@ -548,8 +553,12 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
|
|||||||
err_handler.setFormatter(
|
err_handler.setFormatter(
|
||||||
logging.Formatter('%(asctime)s %(name)s: %(message)s',
|
logging.Formatter('%(asctime)s %(name)s: %(message)s',
|
||||||
datefmt='%y-%m-%d %H:%M:%S'))
|
datefmt='%y-%m-%d %H:%M:%S'))
|
||||||
|
|
||||||
|
async_handler = AsyncHandler(hass.loop, err_handler)
|
||||||
|
hass.data[core.DATA_ASYNCHANDLER] = async_handler
|
||||||
|
|
||||||
logger = logging.getLogger('')
|
logger = logging.getLogger('')
|
||||||
logger.addHandler(err_handler)
|
logger.addHandler(async_handler)
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -4,6 +4,7 @@ Component to interface with an alarm control panel.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/alarm_control_panel/
|
https://home-assistant.io/components/alarm_control_panel/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -42,40 +43,6 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
|
||||||
"""Track states and offer events for sensors."""
|
|
||||||
component = EntityComponent(
|
|
||||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
|
||||||
|
|
||||||
component.setup(config)
|
|
||||||
|
|
||||||
def alarm_service_handler(service):
|
|
||||||
"""Map services to methods on Alarm."""
|
|
||||||
target_alarms = component.extract_from_service(service)
|
|
||||||
|
|
||||||
code = service.data.get(ATTR_CODE)
|
|
||||||
|
|
||||||
method = SERVICE_TO_METHOD[service.service]
|
|
||||||
|
|
||||||
for alarm in target_alarms:
|
|
||||||
getattr(alarm, method)(code)
|
|
||||||
|
|
||||||
for alarm in target_alarms:
|
|
||||||
if not alarm.should_poll:
|
|
||||||
continue
|
|
||||||
|
|
||||||
alarm.update_ha_state(True)
|
|
||||||
|
|
||||||
descriptions = load_yaml_config_file(
|
|
||||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
|
||||||
|
|
||||||
for service in SERVICE_TO_METHOD:
|
|
||||||
hass.services.register(DOMAIN, service, alarm_service_handler,
|
|
||||||
descriptions.get(service),
|
|
||||||
schema=ALARM_SERVICE_SCHEMA)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def alarm_disarm(hass, code=None, entity_id=None):
|
def alarm_disarm(hass, code=None, entity_id=None):
|
||||||
"""Send the alarm the command for disarm."""
|
"""Send the alarm the command for disarm."""
|
||||||
data = {}
|
data = {}
|
||||||
@ -120,6 +87,53 @@ def alarm_trigger(hass, code=None, entity_id=None):
|
|||||||
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
|
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup(hass, config):
|
||||||
|
"""Track states and offer events for sensors."""
|
||||||
|
component = EntityComponent(
|
||||||
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||||
|
|
||||||
|
yield from component.async_setup(config)
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_alarm_service_handler(service):
|
||||||
|
"""Map services to methods on Alarm."""
|
||||||
|
target_alarms = component.async_extract_from_service(service)
|
||||||
|
|
||||||
|
code = service.data.get(ATTR_CODE)
|
||||||
|
|
||||||
|
method = "async_{}".format(SERVICE_TO_METHOD[service.service])
|
||||||
|
|
||||||
|
for alarm in target_alarms:
|
||||||
|
yield from getattr(alarm, method)(code)
|
||||||
|
|
||||||
|
update_tasks = []
|
||||||
|
for alarm in target_alarms:
|
||||||
|
if not alarm.should_poll:
|
||||||
|
continue
|
||||||
|
|
||||||
|
update_coro = hass.loop.create_task(
|
||||||
|
alarm.async_update_ha_state(True))
|
||||||
|
if hasattr(alarm, 'async_update'):
|
||||||
|
update_tasks.append(hass.loop.create_task(update_coro))
|
||||||
|
else:
|
||||||
|
yield from update_coro
|
||||||
|
|
||||||
|
if update_tasks:
|
||||||
|
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||||
|
|
||||||
|
descriptions = yield from hass.loop.run_in_executor(
|
||||||
|
None, load_yaml_config_file, os.path.join(
|
||||||
|
os.path.dirname(__file__), 'services.yaml'))
|
||||||
|
|
||||||
|
for service in SERVICE_TO_METHOD:
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, service, async_alarm_service_handler,
|
||||||
|
descriptions.get(service), schema=ALARM_SERVICE_SCHEMA)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
class AlarmControlPanel(Entity):
|
class AlarmControlPanel(Entity):
|
||||||
"""An abstract class for alarm control devices."""
|
"""An abstract class for alarm control devices."""
|
||||||
@ -138,18 +152,42 @@ class AlarmControlPanel(Entity):
|
|||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_alarm_disarm(self, code=None):
|
||||||
|
"""Send disarm command."""
|
||||||
|
yield from self.hass.loop.run_in_executor(
|
||||||
|
None, self.alarm_disarm, code)
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_alarm_arm_home(self, code=None):
|
||||||
|
"""Send arm home command."""
|
||||||
|
yield from self.hass.loop.run_in_executor(
|
||||||
|
None, self.alarm_arm_home, code)
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_alarm_arm_away(self, code=None):
|
||||||
|
"""Send arm away command."""
|
||||||
|
yield from self.hass.loop.run_in_executor(
|
||||||
|
None, self.alarm_arm_away, code)
|
||||||
|
|
||||||
def alarm_trigger(self, code=None):
|
def alarm_trigger(self, code=None):
|
||||||
"""Send alarm trigger command."""
|
"""Send alarm trigger command."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_alarm_trigger(self, code=None):
|
||||||
|
"""Send alarm trigger command."""
|
||||||
|
yield from self.hass.loop.run_in_executor(
|
||||||
|
None, self.alarm_trigger, code)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
|
@ -56,11 +56,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||||||
self._password = password
|
self._password = password
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
|
|
||||||
@property
|
|
||||||
def should_poll(self):
|
|
||||||
"""No polling needed."""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Fetch the latest state."""
|
"""Fetch the latest state."""
|
||||||
self._state = self._alarm.state
|
self._state = self._alarm.state
|
||||||
|
@ -71,11 +71,6 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
|||||||
self._alarm.last_partition_update = datetime.datetime.now()
|
self._alarm.last_partition_update = datetime.datetime.now()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@property
|
|
||||||
def should_poll(self):
|
|
||||||
"""Polling needed."""
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
@ -126,7 +121,3 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
|||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
self._alarm.arm('auto')
|
self._alarm.arm('auto')
|
||||||
|
|
||||||
def alarm_trigger(self, code=None):
|
|
||||||
"""Alarm trigger command."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
@ -97,7 +97,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
|||||||
def _update_callback(self, partition):
|
def _update_callback(self, partition):
|
||||||
"""Update HA state, if needed."""
|
"""Update HA state, if needed."""
|
||||||
if partition is None or int(partition) == self._partition_number:
|
if partition is None or int(partition) == self._partition_number:
|
||||||
self.hass.async_add_job(self.update_ha_state)
|
self.hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def code_format(self):
|
def code_format(self):
|
||||||
|
@ -116,7 +116,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||||||
|
|
||||||
self._state = STATE_ALARM_DISARMED
|
self._state = STATE_ALARM_DISARMED
|
||||||
self._state_ts = dt_util.utcnow()
|
self._state_ts = dt_util.utcnow()
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
@ -125,7 +125,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||||||
|
|
||||||
self._state = STATE_ALARM_ARMED_HOME
|
self._state = STATE_ALARM_ARMED_HOME
|
||||||
self._state_ts = dt_util.utcnow()
|
self._state_ts = dt_util.utcnow()
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
if self._pending_time:
|
if self._pending_time:
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
@ -139,7 +139,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||||||
|
|
||||||
self._state = STATE_ALARM_ARMED_AWAY
|
self._state = STATE_ALARM_ARMED_AWAY
|
||||||
self._state_ts = dt_util.utcnow()
|
self._state_ts = dt_util.utcnow()
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
if self._pending_time:
|
if self._pending_time:
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
@ -151,7 +151,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||||||
self._pre_trigger_state = self._state
|
self._pre_trigger_state = self._state
|
||||||
self._state = STATE_ALARM_TRIGGERED
|
self._state = STATE_ALARM_TRIGGERED
|
||||||
self._state_ts = dt_util.utcnow()
|
self._state_ts = dt_util.utcnow()
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
if self._trigger_time:
|
if self._trigger_time:
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
|
@ -62,11 +62,6 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
|||||||
self._alarm.list_zones()
|
self._alarm.list_zones()
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
|
|
||||||
@property
|
|
||||||
def should_poll(self):
|
|
||||||
"""Polling needed."""
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
@ -122,7 +117,3 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
|||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
self._alarm.arm('exit')
|
self._alarm.arm('exit')
|
||||||
|
|
||||||
def alarm_trigger(self, code=None):
|
|
||||||
"""Alarm trigger command."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
@ -61,11 +61,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
|||||||
else:
|
else:
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
|
|
||||||
@property
|
|
||||||
def should_poll(self):
|
|
||||||
"""Poll the SimpliSafe API."""
|
|
||||||
return True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
@ -104,7 +99,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
|||||||
return
|
return
|
||||||
self.simplisafe.set_state('off')
|
self.simplisafe.set_state('off')
|
||||||
_LOGGER.info('SimpliSafe alarm disarming')
|
_LOGGER.info('SimpliSafe alarm disarming')
|
||||||
self.update()
|
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
@ -112,7 +106,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
|||||||
return
|
return
|
||||||
self.simplisafe.set_state('home')
|
self.simplisafe.set_state('home')
|
||||||
_LOGGER.info('SimpliSafe alarm arming home')
|
_LOGGER.info('SimpliSafe alarm arming home')
|
||||||
self.update()
|
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
@ -120,7 +113,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
|||||||
return
|
return
|
||||||
self.simplisafe.set_state('away')
|
self.simplisafe.set_state('away')
|
||||||
_LOGGER.info('SimpliSafe alarm arming away')
|
_LOGGER.info('SimpliSafe alarm arming away')
|
||||||
self.update()
|
|
||||||
|
|
||||||
def _validate_code(self, code, state):
|
def _validate_code(self, code, state):
|
||||||
"""Validate given code."""
|
"""Validate given code."""
|
||||||
|
@ -84,18 +84,15 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||||||
hub.my_pages.alarm.set(code, 'DISARMED')
|
hub.my_pages.alarm.set(code, 'DISARMED')
|
||||||
_LOGGER.info('verisure alarm disarming')
|
_LOGGER.info('verisure alarm disarming')
|
||||||
hub.my_pages.alarm.wait_while_pending()
|
hub.my_pages.alarm.wait_while_pending()
|
||||||
self.update()
|
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
hub.my_pages.alarm.set(code, 'ARMED_HOME')
|
hub.my_pages.alarm.set(code, 'ARMED_HOME')
|
||||||
_LOGGER.info('verisure alarm arming home')
|
_LOGGER.info('verisure alarm arming home')
|
||||||
hub.my_pages.alarm.wait_while_pending()
|
hub.my_pages.alarm.wait_while_pending()
|
||||||
self.update()
|
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
hub.my_pages.alarm.set(code, 'ARMED_AWAY')
|
hub.my_pages.alarm.set(code, 'ARMED_AWAY')
|
||||||
_LOGGER.info('verisure alarm arming away')
|
_LOGGER.info('verisure alarm arming away')
|
||||||
hub.my_pages.alarm.wait_while_pending()
|
hub.my_pages.alarm.wait_while_pending()
|
||||||
self.update()
|
|
||||||
|
@ -4,6 +4,8 @@ Offer MQTT listening automation rules.
|
|||||||
For more details about this automation rule, please refer to the documentation
|
For more details about this automation rule, please refer to the documentation
|
||||||
at https://home-assistant.io/components/automation/#mqtt-trigger
|
at https://home-assistant.io/components/automation/#mqtt-trigger
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
@ -31,13 +33,20 @@ def async_trigger(hass, config, action):
|
|||||||
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
def mqtt_automation_listener(msg_topic, msg_payload, qos):
|
||||||
"""Listen for MQTT messages."""
|
"""Listen for MQTT messages."""
|
||||||
if payload is None or payload == msg_payload:
|
if payload is None or payload == msg_payload:
|
||||||
|
data = {
|
||||||
|
'platform': 'mqtt',
|
||||||
|
'topic': msg_topic,
|
||||||
|
'payload': msg_payload,
|
||||||
|
'qos': qos,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
data['payload_json'] = json.loads(msg_payload)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
hass.async_run_job(action, {
|
hass.async_run_job(action, {
|
||||||
'trigger': {
|
'trigger': data
|
||||||
'platform': 'mqtt',
|
|
||||||
'topic': msg_topic,
|
|
||||||
'payload': msg_payload,
|
|
||||||
'qos': qos,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return mqtt.async_subscribe(hass, topic, mqtt_automation_listener)
|
return mqtt.async_subscribe(hass, topic, mqtt_automation_listener)
|
||||||
|
@ -64,10 +64,19 @@ def async_trigger(hass, config, action):
|
|||||||
call_action()
|
call_action()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def clear_listener():
|
||||||
|
"""Clear all unsub listener."""
|
||||||
|
nonlocal async_remove_state_for_cancel
|
||||||
|
nonlocal async_remove_state_for_listener
|
||||||
|
async_remove_state_for_listener = None
|
||||||
|
async_remove_state_for_cancel = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def state_for_listener(now):
|
def state_for_listener(now):
|
||||||
"""Fire on state changes after a delay and calls action."""
|
"""Fire on state changes after a delay and calls action."""
|
||||||
async_remove_state_for_cancel()
|
async_remove_state_for_cancel()
|
||||||
|
clear_listener()
|
||||||
call_action()
|
call_action()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -77,6 +86,7 @@ def async_trigger(hass, config, action):
|
|||||||
return
|
return
|
||||||
async_remove_state_for_listener()
|
async_remove_state_for_listener()
|
||||||
async_remove_state_for_cancel()
|
async_remove_state_for_cancel()
|
||||||
|
clear_listener()
|
||||||
|
|
||||||
async_remove_state_for_listener = async_track_point_in_utc_time(
|
async_remove_state_for_listener = async_track_point_in_utc_time(
|
||||||
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
||||||
|
257
homeassistant/components/binary_sensor/flic.py
Normal file
257
homeassistant/components/binary_sensor/flic.py
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
"""Contains functionality to use flic buttons as a binary sensor."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST, CONF_PORT, CONF_DISCOVERY, CONF_TIMEOUT,
|
||||||
|
EVENT_HOMEASSISTANT_STOP)
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||||
|
from homeassistant.util.async import run_callback_threadsafe
|
||||||
|
|
||||||
|
|
||||||
|
REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT = 3
|
||||||
|
|
||||||
|
CLICK_TYPE_SINGLE = "single"
|
||||||
|
CLICK_TYPE_DOUBLE = "double"
|
||||||
|
CLICK_TYPE_HOLD = "hold"
|
||||||
|
CLICK_TYPES = [CLICK_TYPE_SINGLE, CLICK_TYPE_DOUBLE, CLICK_TYPE_HOLD]
|
||||||
|
|
||||||
|
CONF_IGNORED_CLICK_TYPES = "ignored_click_types"
|
||||||
|
|
||||||
|
EVENT_NAME = "flic_click"
|
||||||
|
EVENT_DATA_NAME = "button_name"
|
||||||
|
EVENT_DATA_ADDRESS = "button_address"
|
||||||
|
EVENT_DATA_TYPE = "click_type"
|
||||||
|
EVENT_DATA_QUEUED_TIME = "queued_time"
|
||||||
|
|
||||||
|
# Validation of the user's configuration
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_HOST, default='localhost'): cv.string,
|
||||||
|
vol.Optional(CONF_PORT, default=5551): cv.port,
|
||||||
|
vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
|
||||||
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||||
|
vol.Optional(CONF_IGNORED_CLICK_TYPES): vol.All(cv.ensure_list,
|
||||||
|
[vol.In(CLICK_TYPES)])
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_platform(hass, config, async_add_entities,
|
||||||
|
discovery_info=None):
|
||||||
|
"""Setup the flic platform."""
|
||||||
|
import pyflic
|
||||||
|
|
||||||
|
# Initialize flic client responsible for
|
||||||
|
# connecting to buttons and retrieving events
|
||||||
|
host = config.get(CONF_HOST)
|
||||||
|
port = config.get(CONF_PORT)
|
||||||
|
discovery = config.get(CONF_DISCOVERY)
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = pyflic.FlicClient(host, port)
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
_LOGGER.error("Failed to connect to flic server.")
|
||||||
|
return
|
||||||
|
|
||||||
|
def new_button_callback(address):
|
||||||
|
"""Setup newly verified button as device in home assistant."""
|
||||||
|
hass.add_job(async_setup_button(hass, config, async_add_entities,
|
||||||
|
client, address))
|
||||||
|
|
||||||
|
client.on_new_verified_button = new_button_callback
|
||||||
|
if discovery:
|
||||||
|
start_scanning(hass, config, async_add_entities, client)
|
||||||
|
|
||||||
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||||
|
lambda event: client.close())
|
||||||
|
hass.loop.run_in_executor(None, client.handle_events)
|
||||||
|
|
||||||
|
# Get addresses of already verified buttons
|
||||||
|
addresses = yield from async_get_verified_addresses(client)
|
||||||
|
if addresses:
|
||||||
|
for address in addresses:
|
||||||
|
yield from async_setup_button(hass, config, async_add_entities,
|
||||||
|
client, address)
|
||||||
|
|
||||||
|
|
||||||
|
def start_scanning(hass, config, async_add_entities, client):
|
||||||
|
"""Start a new flic client for scanning & connceting to new buttons."""
|
||||||
|
import pyflic
|
||||||
|
|
||||||
|
scan_wizard = pyflic.ScanWizard()
|
||||||
|
|
||||||
|
def scan_completed_callback(scan_wizard, result, address, name):
|
||||||
|
"""Restart scan wizard to constantly check for new buttons."""
|
||||||
|
if result == pyflic.ScanWizardResult.WizardSuccess:
|
||||||
|
_LOGGER.info("Found new button (%s)", address)
|
||||||
|
elif result != pyflic.ScanWizardResult.WizardFailedTimeout:
|
||||||
|
_LOGGER.warning("Failed to connect to button (%s). Reason: %s",
|
||||||
|
address, result)
|
||||||
|
|
||||||
|
# Restart scan wizard
|
||||||
|
start_scanning(hass, config, async_add_entities, client)
|
||||||
|
|
||||||
|
scan_wizard.on_completed = scan_completed_callback
|
||||||
|
client.add_scan_wizard(scan_wizard)
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_setup_button(hass, config, async_add_entities, client, address):
|
||||||
|
"""Setup single button device."""
|
||||||
|
timeout = config.get(CONF_TIMEOUT)
|
||||||
|
ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES)
|
||||||
|
button = FlicButton(hass, client, address, timeout, ignored_click_types)
|
||||||
|
_LOGGER.info("Connected to button (%s)", address)
|
||||||
|
|
||||||
|
yield from async_add_entities([button])
|
||||||
|
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_get_verified_addresses(client):
|
||||||
|
"""Retrieve addresses of verified buttons."""
|
||||||
|
future = asyncio.Future()
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
def get_info_callback(items):
|
||||||
|
"""Set the addressed of connected buttons as result of the future."""
|
||||||
|
addresses = items["bd_addr_of_verified_buttons"]
|
||||||
|
run_callback_threadsafe(loop, future.set_result, addresses)
|
||||||
|
client.get_info(get_info_callback)
|
||||||
|
|
||||||
|
return future
|
||||||
|
|
||||||
|
|
||||||
|
class FlicButton(BinarySensorDevice):
|
||||||
|
"""Representation of a flic button."""
|
||||||
|
|
||||||
|
def __init__(self, hass, client, address, timeout, ignored_click_types):
|
||||||
|
"""Initialize the flic button."""
|
||||||
|
import pyflic
|
||||||
|
|
||||||
|
self._hass = hass
|
||||||
|
self._address = address
|
||||||
|
self._timeout = timeout
|
||||||
|
self._is_down = False
|
||||||
|
self._ignored_click_types = ignored_click_types or []
|
||||||
|
self._hass_click_types = {
|
||||||
|
pyflic.ClickType.ButtonClick: CLICK_TYPE_SINGLE,
|
||||||
|
pyflic.ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE,
|
||||||
|
pyflic.ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE,
|
||||||
|
pyflic.ClickType.ButtonHold: CLICK_TYPE_HOLD,
|
||||||
|
}
|
||||||
|
|
||||||
|
self._channel = self._create_channel()
|
||||||
|
client.add_connection_channel(self._channel)
|
||||||
|
|
||||||
|
def _create_channel(self):
|
||||||
|
"""Create a new connection channel to the button."""
|
||||||
|
import pyflic
|
||||||
|
|
||||||
|
channel = pyflic.ButtonConnectionChannel(self._address)
|
||||||
|
channel.on_button_up_or_down = self._on_up_down
|
||||||
|
|
||||||
|
# If all types of clicks should be ignored, skip registering callbacks
|
||||||
|
if set(self._ignored_click_types) == set(CLICK_TYPES):
|
||||||
|
return channel
|
||||||
|
|
||||||
|
if CLICK_TYPE_DOUBLE in self._ignored_click_types:
|
||||||
|
# Listen to all but double click type events
|
||||||
|
channel.on_button_click_or_hold = self._on_click
|
||||||
|
elif CLICK_TYPE_HOLD in self._ignored_click_types:
|
||||||
|
# Listen to all but hold click type events
|
||||||
|
channel.on_button_single_or_double_click = self._on_click
|
||||||
|
else:
|
||||||
|
# Listen to all click type events
|
||||||
|
channel.on_button_single_or_double_click_or_hold = self._on_click
|
||||||
|
|
||||||
|
return channel
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return "flic_%s" % self.address.replace(":", "")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def address(self):
|
||||||
|
"""Return the bluetooth address of the device."""
|
||||||
|
return self._address
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if sensor is on."""
|
||||||
|
return self._is_down
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
"""Return device specific state attributes."""
|
||||||
|
attr = super(FlicButton, self).state_attributes
|
||||||
|
attr["address"] = self.address
|
||||||
|
|
||||||
|
return attr
|
||||||
|
|
||||||
|
def _queued_event_check(self, click_type, time_diff):
|
||||||
|
"""Generate a log message and returns true if timeout exceeded."""
|
||||||
|
time_string = "{:d} {}".format(
|
||||||
|
time_diff, "second" if time_diff == 1 else "seconds")
|
||||||
|
|
||||||
|
if time_diff > self._timeout:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Queued %s dropped for %s. Time in queue was %s.",
|
||||||
|
click_type, self.address, time_string)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Queued %s allowed for %s. Time in queue was %s.",
|
||||||
|
click_type, self.address, time_string)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _on_up_down(self, channel, click_type, was_queued, time_diff):
|
||||||
|
"""Update device state, if event was not queued."""
|
||||||
|
import pyflic
|
||||||
|
|
||||||
|
if was_queued and self._queued_event_check(click_type, time_diff):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._is_down = click_type == pyflic.ClickType.ButtonDown
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def _on_click(self, channel, click_type, was_queued, time_diff):
|
||||||
|
"""Fire click event, if event was not queued."""
|
||||||
|
# Return if click event was queued beyond allowed timeout
|
||||||
|
if was_queued and self._queued_event_check(click_type, time_diff):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Return if click event is in ignored click types
|
||||||
|
hass_click_type = self._hass_click_types[click_type]
|
||||||
|
if hass_click_type in self._ignored_click_types:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._hass.bus.fire(EVENT_NAME, {
|
||||||
|
EVENT_DATA_NAME: self.name,
|
||||||
|
EVENT_DATA_ADDRESS: self.address,
|
||||||
|
EVENT_DATA_QUEUED_TIME: time_diff,
|
||||||
|
EVENT_DATA_TYPE: hass_click_type
|
||||||
|
})
|
||||||
|
|
||||||
|
def _connection_status_changed(self, channel,
|
||||||
|
connection_status, disconnect_reason):
|
||||||
|
"""Remove device, if button disconnects."""
|
||||||
|
import pyflic
|
||||||
|
|
||||||
|
if connection_status == pyflic.ConnectionStatus.Disconnected:
|
||||||
|
_LOGGER.info("Button (%s) disconnected. Reason: %s",
|
||||||
|
self.address, disconnect_reason)
|
||||||
|
self.remove()
|
262
homeassistant/components/binary_sensor/hikvision.py
Normal file
262
homeassistant/components/binary_sensor/hikvision.py
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
"""
|
||||||
|
Support for Hikvision event stream events represented as binary sensors.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/binary_sensor.hikvision/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.helpers.event import track_point_in_utc_time
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
|
||||||
|
CONF_SSL, EVENT_HOMEASSISTANT_STOP, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['pyhik==0.0.6', 'pydispatcher==2.0.5']
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_IGNORED = 'ignored'
|
||||||
|
CONF_DELAY = 'delay'
|
||||||
|
|
||||||
|
DEFAULT_PORT = 80
|
||||||
|
DEFAULT_IGNORED = False
|
||||||
|
DEFAULT_DELAY = 0
|
||||||
|
|
||||||
|
ATTR_DELAY = 'delay'
|
||||||
|
|
||||||
|
SENSOR_CLASS_MAP = {
|
||||||
|
'Motion': 'motion',
|
||||||
|
'Line Crossing': 'motion',
|
||||||
|
'IO Trigger': None,
|
||||||
|
'Field Detection': 'motion',
|
||||||
|
'Video Loss': None,
|
||||||
|
'Tamper Detection': 'motion',
|
||||||
|
'Shelter Alarm': None,
|
||||||
|
'Disk Full': None,
|
||||||
|
'Disk Error': None,
|
||||||
|
'Net Interface Broken': 'connectivity',
|
||||||
|
'IP Conflict': 'connectivity',
|
||||||
|
'Illegal Access': None,
|
||||||
|
'Video Mismatch': None,
|
||||||
|
'Bad Video': None,
|
||||||
|
'PIR Alarm': 'motion',
|
||||||
|
'Face Detection': 'motion',
|
||||||
|
}
|
||||||
|
|
||||||
|
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CONF_IGNORED, default=DEFAULT_IGNORED): cv.boolean,
|
||||||
|
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int
|
||||||
|
})
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_NAME, default=None): cv.string,
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
|
vol.Optional(CONF_SSL, default=False): cv.boolean,
|
||||||
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
vol.Optional(CONF_CUSTOMIZE, default={}):
|
||||||
|
vol.Schema({cv.string: CUSTOMIZE_SCHEMA}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
"""Setup Hikvision binary sensor devices."""
|
||||||
|
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)
|
||||||
|
|
||||||
|
customize = config.get(CONF_CUSTOMIZE)
|
||||||
|
|
||||||
|
if config.get(CONF_SSL):
|
||||||
|
protocol = "https"
|
||||||
|
else:
|
||||||
|
protocol = "http"
|
||||||
|
|
||||||
|
url = '{}://{}'.format(protocol, host)
|
||||||
|
|
||||||
|
data = HikvisionData(hass, url, port, name, username, password)
|
||||||
|
|
||||||
|
if data.sensors is None:
|
||||||
|
_LOGGER.error('Hikvision event stream has no data, unable to setup.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
entities = []
|
||||||
|
|
||||||
|
for sensor in data.sensors:
|
||||||
|
# Build sensor name, then parse customize config.
|
||||||
|
sensor_name = sensor.replace(' ', '_')
|
||||||
|
|
||||||
|
custom = customize.get(sensor_name.lower(), {})
|
||||||
|
ignore = custom.get(CONF_IGNORED)
|
||||||
|
delay = custom.get(CONF_DELAY)
|
||||||
|
|
||||||
|
_LOGGER.debug('Entity: %s - %s, Options - Ignore: %s, Delay: %s',
|
||||||
|
data.name, sensor_name, ignore, delay)
|
||||||
|
if not ignore:
|
||||||
|
entities.append(HikvisionBinarySensor(hass, sensor, data, delay))
|
||||||
|
|
||||||
|
add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class HikvisionData(object):
|
||||||
|
"""Hikvision camera event stream object."""
|
||||||
|
|
||||||
|
def __init__(self, hass, url, port, name, username, password):
|
||||||
|
"""Initialize the data oject."""
|
||||||
|
from pyhik.hikvision import HikCamera
|
||||||
|
self._url = url
|
||||||
|
self._port = port
|
||||||
|
self._name = name
|
||||||
|
self._username = username
|
||||||
|
self._password = password
|
||||||
|
|
||||||
|
# Establish camera
|
||||||
|
self._cam = HikCamera(self._url, self._port,
|
||||||
|
self._username, self._password)
|
||||||
|
|
||||||
|
if self._name is None:
|
||||||
|
self._name = self._cam.get_name
|
||||||
|
|
||||||
|
# Start event stream
|
||||||
|
self._cam.start_stream()
|
||||||
|
|
||||||
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik)
|
||||||
|
|
||||||
|
def stop_hik(self, event):
|
||||||
|
"""Shutdown Hikvision subscriptions and subscription thread on exit."""
|
||||||
|
self._cam.disconnect()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sensors(self):
|
||||||
|
"""Return list of available sensors and their states."""
|
||||||
|
return self._cam.current_event_states
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cam_id(self):
|
||||||
|
"""Return camera id."""
|
||||||
|
return self._cam.get_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return camera name."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
|
||||||
|
class HikvisionBinarySensor(BinarySensorDevice):
|
||||||
|
"""Representation of a Hikvision binary sensor."""
|
||||||
|
|
||||||
|
def __init__(self, hass, sensor, cam, delay):
|
||||||
|
"""Initialize the binary_sensor."""
|
||||||
|
from pydispatch import dispatcher
|
||||||
|
|
||||||
|
self._hass = hass
|
||||||
|
self._cam = cam
|
||||||
|
self._name = self._cam.name + ' ' + sensor
|
||||||
|
self._id = self._cam.cam_id + '.' + sensor
|
||||||
|
self._sensor = sensor
|
||||||
|
|
||||||
|
if delay is None:
|
||||||
|
self._delay = 0
|
||||||
|
else:
|
||||||
|
self._delay = delay
|
||||||
|
|
||||||
|
self._timer = None
|
||||||
|
|
||||||
|
# Form signal for dispatcher
|
||||||
|
signal = 'ValueChanged.{}'.format(self._cam.cam_id)
|
||||||
|
|
||||||
|
dispatcher.connect(self._update_callback,
|
||||||
|
signal=signal,
|
||||||
|
sender=self._sensor)
|
||||||
|
|
||||||
|
def _sensor_state(self):
|
||||||
|
"""Extract sensor state."""
|
||||||
|
return self._cam.sensors[self._sensor][0]
|
||||||
|
|
||||||
|
def _sensor_last_update(self):
|
||||||
|
"""Extract sensor last update time."""
|
||||||
|
return self._cam.sensors[self._sensor][3]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the Hikvision sensor."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return an unique ID."""
|
||||||
|
return '{}.{}'.format(self.__class__, self._id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if sensor is on."""
|
||||||
|
return self._sensor_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sensor_class(self):
|
||||||
|
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||||
|
try:
|
||||||
|
return SENSOR_CLASS_MAP[self._sensor]
|
||||||
|
except KeyError:
|
||||||
|
# Sensor must be unknown to us, add as generic
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No polling needed."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
attr = {}
|
||||||
|
attr[ATTR_LAST_TRIP_TIME] = self._sensor_last_update()
|
||||||
|
|
||||||
|
if self._delay != 0:
|
||||||
|
attr[ATTR_DELAY] = self._delay
|
||||||
|
|
||||||
|
return attr
|
||||||
|
|
||||||
|
def _update_callback(self, signal, sender):
|
||||||
|
"""Update the sensor's state, if needed."""
|
||||||
|
_LOGGER.debug('Dispatcher callback, signal: %s, sender: %s',
|
||||||
|
signal, sender)
|
||||||
|
|
||||||
|
if sender is not self._sensor:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._delay > 0 and not self.is_on:
|
||||||
|
# Set timer to wait until updating the state
|
||||||
|
def _delay_update(now):
|
||||||
|
"""Timer callback for sensor update."""
|
||||||
|
_LOGGER.debug('%s Called delayed (%ssec) update.',
|
||||||
|
self._name, self._delay)
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
self._timer = None
|
||||||
|
|
||||||
|
if self._timer is not None:
|
||||||
|
self._timer()
|
||||||
|
self._timer = None
|
||||||
|
|
||||||
|
self._timer = track_point_in_utc_time(
|
||||||
|
self._hass, _delay_update,
|
||||||
|
utcnow() + timedelta(seconds=self._delay))
|
||||||
|
|
||||||
|
elif self._delay > 0 and self.is_on:
|
||||||
|
# For delayed sensors kill any callbacks on true events and update
|
||||||
|
if self._timer is not None:
|
||||||
|
self._timer()
|
||||||
|
self._timer = None
|
||||||
|
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.schedule_update_ha_state()
|
@ -23,9 +23,11 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# These are the available sensors mapped to binary_sensor class
|
# These are the available sensors mapped to binary_sensor class
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
"Someone known": "motion",
|
"Someone known": 'occupancy',
|
||||||
"Someone unknown": "motion",
|
"Someone unknown": 'motion',
|
||||||
"Motion": "motion",
|
"Motion": 'motion',
|
||||||
|
"Tag Vibration": 'vibration',
|
||||||
|
"Tag Open": 'opening',
|
||||||
}
|
}
|
||||||
|
|
||||||
CONF_HOME = 'home'
|
CONF_HOME = 'home'
|
||||||
@ -48,6 +50,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
home = config.get(CONF_HOME, None)
|
home = config.get(CONF_HOME, None)
|
||||||
timeout = config.get(CONF_TIMEOUT, 15)
|
timeout = config.get(CONF_TIMEOUT, 15)
|
||||||
|
|
||||||
|
module_name = None
|
||||||
|
|
||||||
import lnetatmo
|
import lnetatmo
|
||||||
try:
|
try:
|
||||||
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
data = WelcomeData(netatmo.NETATMO_AUTH, home)
|
||||||
@ -64,23 +68,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
camera_name not in config[CONF_CAMERAS]:
|
camera_name not in config[CONF_CAMERAS]:
|
||||||
continue
|
continue
|
||||||
for variable in sensors:
|
for variable in sensors:
|
||||||
add_devices([WelcomeBinarySensor(data, camera_name, home, timeout,
|
if variable in ('Tag Vibration', 'Tag Open'):
|
||||||
variable)])
|
continue
|
||||||
|
add_devices([WelcomeBinarySensor(data, camera_name, module_name,
|
||||||
|
home, timeout, variable)])
|
||||||
|
|
||||||
|
for module_name in data.get_module_names(camera_name):
|
||||||
|
for variable in sensors:
|
||||||
|
if variable in ('Tag Vibration', 'Tag Open'):
|
||||||
|
add_devices([WelcomeBinarySensor(data, camera_name,
|
||||||
|
module_name, home,
|
||||||
|
timeout, variable)])
|
||||||
|
|
||||||
|
|
||||||
class WelcomeBinarySensor(BinarySensorDevice):
|
class WelcomeBinarySensor(BinarySensorDevice):
|
||||||
"""Represent a single binary sensor in a Netatmo Welcome device."""
|
"""Represent a single binary sensor in a Netatmo Welcome device."""
|
||||||
|
|
||||||
def __init__(self, data, camera_name, home, timeout, sensor):
|
def __init__(self, data, camera_name, module_name, home, timeout, sensor):
|
||||||
"""Setup for access to the Netatmo camera events."""
|
"""Setup for access to the Netatmo camera events."""
|
||||||
self._data = data
|
self._data = data
|
||||||
self._camera_name = camera_name
|
self._camera_name = camera_name
|
||||||
|
self._module_name = module_name
|
||||||
self._home = home
|
self._home = home
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
if home:
|
if home:
|
||||||
self._name = home + ' / ' + camera_name
|
self._name = home + ' / ' + camera_name
|
||||||
else:
|
else:
|
||||||
self._name = camera_name
|
self._name = camera_name
|
||||||
|
if module_name:
|
||||||
|
self._name += ' / ' + module_name
|
||||||
self._sensor_name = sensor
|
self._sensor_name = sensor
|
||||||
self._name += ' ' + sensor
|
self._name += ' ' + sensor
|
||||||
camera_id = data.welcomedata.cameraByName(camera=camera_name,
|
camera_id = data.welcomedata.cameraByName(camera=camera_name,
|
||||||
@ -112,7 +128,7 @@ class WelcomeBinarySensor(BinarySensorDevice):
|
|||||||
def update(self):
|
def update(self):
|
||||||
"""Request an update from the Netatmo API."""
|
"""Request an update from the Netatmo API."""
|
||||||
self._data.update()
|
self._data.update()
|
||||||
self._data.welcomedata.updateEvent(home=self._data.home)
|
self._data.update_event()
|
||||||
|
|
||||||
if self._sensor_name == "Someone known":
|
if self._sensor_name == "Someone known":
|
||||||
self._state =\
|
self._state =\
|
||||||
@ -129,5 +145,16 @@ class WelcomeBinarySensor(BinarySensorDevice):
|
|||||||
self._data.welcomedata.motionDetected(self._home,
|
self._data.welcomedata.motionDetected(self._home,
|
||||||
self._camera_name,
|
self._camera_name,
|
||||||
self._timeout*60)
|
self._timeout*60)
|
||||||
|
elif self._sensor_name == "Tag Vibration":
|
||||||
|
self._state =\
|
||||||
|
self._data.welcomedata.moduleMotionDetected(self._home,
|
||||||
|
self._module_name,
|
||||||
|
self._camera_name,
|
||||||
|
self._timeout*60)
|
||||||
|
elif self._sensor_name == "Tag Open":
|
||||||
|
self._state =\
|
||||||
|
self._data.welcomedata.moduleOpened(self._home,
|
||||||
|
self._module_name,
|
||||||
|
self._camera_name)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -40,6 +40,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
for sensor in pywink.get_smoke_and_co_detectors():
|
for sensor in pywink.get_smoke_and_co_detectors():
|
||||||
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||||
|
|
||||||
|
for hub in pywink.get_hubs():
|
||||||
|
add_devices([WinkHub(hub, hass)])
|
||||||
|
|
||||||
|
|
||||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||||
"""Representation of a Wink binary sensor."""
|
"""Representation of a Wink binary sensor."""
|
||||||
@ -79,3 +82,24 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
|||||||
def sensor_class(self):
|
def sensor_class(self):
|
||||||
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
"""Return the class of this sensor, from SENSOR_CLASSES."""
|
||||||
return SENSOR_TYPES.get(self.capability)
|
return SENSOR_TYPES.get(self.capability)
|
||||||
|
|
||||||
|
|
||||||
|
class WinkHub(WinkDevice, BinarySensorDevice, Entity):
|
||||||
|
"""Representation of a Wink Hub."""
|
||||||
|
|
||||||
|
def __init(self, wink, hass):
|
||||||
|
"""Initialize the hub sensor."""
|
||||||
|
WinkDevice.__init__(self, wink, hass)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
return {
|
||||||
|
'update needed': self.wink.update_needed(),
|
||||||
|
'firmware version': self.wink.firmware_version()
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if the binary sensor is on."""
|
||||||
|
return self.wink.state()
|
||||||
|
@ -18,16 +18,26 @@ REQUIREMENTS = ['amcrest==1.0.0']
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_PORT = 80
|
CONF_RESOLUTION = 'resolution'
|
||||||
|
|
||||||
DEFAULT_NAME = 'Amcrest Camera'
|
DEFAULT_NAME = 'Amcrest Camera'
|
||||||
|
DEFAULT_PORT = 80
|
||||||
|
DEFAULT_RESOLUTION = 'high'
|
||||||
|
|
||||||
NOTIFICATION_ID = 'amcrest_notification'
|
NOTIFICATION_ID = 'amcrest_notification'
|
||||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||||
|
|
||||||
|
RESOLUTION_LIST = {
|
||||||
|
'high': 0,
|
||||||
|
'low': 1,
|
||||||
|
}
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||||
|
vol.All(vol.In(RESOLUTION_LIST)),
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
})
|
})
|
||||||
@ -64,13 +74,14 @@ class AmcrestCam(Camera):
|
|||||||
def __init__(self, device_info, data):
|
def __init__(self, device_info, data):
|
||||||
"""Initialize an Amcrest camera."""
|
"""Initialize an Amcrest camera."""
|
||||||
super(AmcrestCam, self).__init__()
|
super(AmcrestCam, self).__init__()
|
||||||
self._name = device_info.get(CONF_NAME)
|
|
||||||
self._data = data
|
self._data = data
|
||||||
|
self._name = device_info.get(CONF_NAME)
|
||||||
|
self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)]
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
"""Return a still image reponse from the camera."""
|
"""Return a still image reponse from the camera."""
|
||||||
# Send the request to snap a picture and return raw jpg data
|
# Send the request to snap a picture and return raw jpg data
|
||||||
response = self._data.camera.snapshot()
|
response = self._data.camera.snapshot(channel=self._resolution)
|
||||||
return response.data
|
return response.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -39,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.Optional(CONF_FILE_PATH): cv.string,
|
vol.Optional(CONF_FILE_PATH): cv.string,
|
||||||
vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
|
vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
|
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
|
||||||
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP):
|
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT):
|
||||||
vol.Coerce(int),
|
vol.Coerce(int),
|
||||||
vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITIY):
|
vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITIY):
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
|
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
|
||||||
|
@ -195,8 +195,9 @@ class Thermostat(ClimateDevice):
|
|||||||
mode = self.mode
|
mode = self.mode
|
||||||
events = self.thermostat['events']
|
events = self.thermostat['events']
|
||||||
for event in events:
|
for event in events:
|
||||||
if event['running']:
|
if event['holdClimateRef'] == 'away' or \
|
||||||
mode = event['holdClimateRef']
|
event['type'] == 'autoAway':
|
||||||
|
mode = "away"
|
||||||
break
|
break
|
||||||
return 'away' in mode
|
return 'away' in mode
|
||||||
|
|
||||||
|
@ -198,24 +198,30 @@ class GenericThermostat(ClimateDevice):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.ac_mode:
|
if self.ac_mode:
|
||||||
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
|
||||||
is_cooling = self._is_device_active
|
is_cooling = self._is_device_active
|
||||||
if too_hot and not is_cooling:
|
if is_cooling:
|
||||||
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
|
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||||
switch.turn_on(self.hass, self.heater_entity_id)
|
if too_cold:
|
||||||
elif not too_hot and is_cooling:
|
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
|
||||||
_LOGGER.info('Turning off AC %s', self.heater_entity_id)
|
switch.turn_off(self.hass, self.heater_entity_id)
|
||||||
switch.turn_off(self.hass, self.heater_entity_id)
|
else:
|
||||||
|
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
||||||
|
if too_hot:
|
||||||
|
_LOGGER.info('Turning on AC %s', self.heater_entity_id)
|
||||||
|
switch.turn_on(self.hass, self.heater_entity_id)
|
||||||
else:
|
else:
|
||||||
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
|
||||||
is_heating = self._is_device_active
|
is_heating = self._is_device_active
|
||||||
|
if is_heating:
|
||||||
if too_cold and not is_heating:
|
too_hot = self._cur_temp - self._target_temp > self._tolerance
|
||||||
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
|
if too_hot:
|
||||||
switch.turn_on(self.hass, self.heater_entity_id)
|
_LOGGER.info('Turning off heater %s',
|
||||||
elif not too_cold and is_heating:
|
self.heater_entity_id)
|
||||||
_LOGGER.info('Turning off heater %s', self.heater_entity_id)
|
switch.turn_off(self.hass, self.heater_entity_id)
|
||||||
switch.turn_off(self.hass, self.heater_entity_id)
|
else:
|
||||||
|
too_cold = self._target_temp - self._cur_temp > self._tolerance
|
||||||
|
if too_cold:
|
||||||
|
_LOGGER.info('Turning on heater %s', self.heater_entity_id)
|
||||||
|
switch.turn_on(self.hass, self.heater_entity_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _is_device_active(self):
|
def _is_device_active(self):
|
||||||
|
@ -155,8 +155,8 @@ class NestThermostat(ClimateDevice):
|
|||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||||
if target_temp_low is not None and target_temp_high is not None:
|
if self._mode == STATE_HEAT_COOL:
|
||||||
if self._mode == STATE_HEAT_COOL:
|
if target_temp_low is not None and target_temp_high is not None:
|
||||||
temp = (target_temp_low, target_temp_high)
|
temp = (target_temp_low, target_temp_high)
|
||||||
else:
|
else:
|
||||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||||
|
@ -23,10 +23,19 @@ ATTR_FAN = 'fan'
|
|||||||
ATTR_MODE = 'mode'
|
ATTR_MODE = 'mode'
|
||||||
|
|
||||||
CONF_HOLD_TEMP = 'hold_temp'
|
CONF_HOLD_TEMP = 'hold_temp'
|
||||||
|
CONF_AWAY_TEMPERATURE_HEAT = 'away_temperature_heat'
|
||||||
|
CONF_AWAY_TEMPERATURE_COOL = 'away_temperature_cool'
|
||||||
|
|
||||||
|
DEFAULT_AWAY_TEMPERATURE_HEAT = 60
|
||||||
|
DEFAULT_AWAY_TEMPERATURE_COOL = 85
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]),
|
vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]),
|
||||||
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean,
|
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean,
|
||||||
|
vol.Optional(CONF_AWAY_TEMPERATURE_HEAT,
|
||||||
|
default=DEFAULT_AWAY_TEMPERATURE_HEAT): vol.Coerce(float),
|
||||||
|
vol.Optional(CONF_AWAY_TEMPERATURE_COOL,
|
||||||
|
default=DEFAULT_AWAY_TEMPERATURE_COOL): vol.Coerce(float),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -45,12 +54,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
hold_temp = config.get(CONF_HOLD_TEMP)
|
hold_temp = config.get(CONF_HOLD_TEMP)
|
||||||
|
away_temps = [
|
||||||
|
config.get(CONF_AWAY_TEMPERATURE_HEAT),
|
||||||
|
config.get(CONF_AWAY_TEMPERATURE_COOL)
|
||||||
|
]
|
||||||
tstats = []
|
tstats = []
|
||||||
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
try:
|
try:
|
||||||
tstat = radiotherm.get_thermostat(host)
|
tstat = radiotherm.get_thermostat(host)
|
||||||
tstats.append(RadioThermostat(tstat, hold_temp))
|
tstats.append(RadioThermostat(tstat, hold_temp, away_temps))
|
||||||
except OSError:
|
except OSError:
|
||||||
_LOGGER.exception("Unable to connect to Radio Thermostat: %s",
|
_LOGGER.exception("Unable to connect to Radio Thermostat: %s",
|
||||||
host)
|
host)
|
||||||
@ -61,7 +74,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
class RadioThermostat(ClimateDevice):
|
class RadioThermostat(ClimateDevice):
|
||||||
"""Representation of a Radio Thermostat."""
|
"""Representation of a Radio Thermostat."""
|
||||||
|
|
||||||
def __init__(self, device, hold_temp):
|
def __init__(self, device, hold_temp, away_temps):
|
||||||
"""Initialize the thermostat."""
|
"""Initialize the thermostat."""
|
||||||
self.device = device
|
self.device = device
|
||||||
self.set_time()
|
self.set_time()
|
||||||
@ -71,7 +84,10 @@ class RadioThermostat(ClimateDevice):
|
|||||||
self._name = None
|
self._name = None
|
||||||
self._fmode = None
|
self._fmode = None
|
||||||
self._tmode = None
|
self._tmode = None
|
||||||
self.hold_temp = hold_temp
|
self._hold_temp = hold_temp
|
||||||
|
self._away = False
|
||||||
|
self._away_temps = away_temps
|
||||||
|
self._prev_temp = None
|
||||||
self.update()
|
self.update()
|
||||||
self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
|
self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
|
||||||
|
|
||||||
@ -113,6 +129,11 @@ class RadioThermostat(ClimateDevice):
|
|||||||
"""Return the temperature we try to reach."""
|
"""Return the temperature we try to reach."""
|
||||||
return self._target_temperature
|
return self._target_temperature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_away_mode_on(self):
|
||||||
|
"""Return true if away mode is on."""
|
||||||
|
return self._away
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update the data from the thermostat."""
|
"""Update the data from the thermostat."""
|
||||||
self._current_temperature = self.device.temp['raw']
|
self._current_temperature = self.device.temp['raw']
|
||||||
@ -138,7 +159,7 @@ class RadioThermostat(ClimateDevice):
|
|||||||
self.device.t_cool = round(temperature * 2.0) / 2.0
|
self.device.t_cool = round(temperature * 2.0) / 2.0
|
||||||
elif self._current_operation == STATE_HEAT:
|
elif self._current_operation == STATE_HEAT:
|
||||||
self.device.t_heat = round(temperature * 2.0) / 2.0
|
self.device.t_heat = round(temperature * 2.0) / 2.0
|
||||||
if self.hold_temp:
|
if self._hold_temp or self._away:
|
||||||
self.device.hold = 1
|
self.device.hold = 1
|
||||||
else:
|
else:
|
||||||
self.device.hold = 0
|
self.device.hold = 0
|
||||||
@ -162,3 +183,23 @@ class RadioThermostat(ClimateDevice):
|
|||||||
self.device.t_cool = round(self._target_temperature * 2.0) / 2.0
|
self.device.t_cool = round(self._target_temperature * 2.0) / 2.0
|
||||||
elif operation_mode == STATE_HEAT:
|
elif operation_mode == STATE_HEAT:
|
||||||
self.device.t_heat = round(self._target_temperature * 2.0) / 2.0
|
self.device.t_heat = round(self._target_temperature * 2.0) / 2.0
|
||||||
|
|
||||||
|
def turn_away_mode_on(self):
|
||||||
|
"""Turn away on.
|
||||||
|
|
||||||
|
The RTCOA app simulates away mode by using a hold.
|
||||||
|
"""
|
||||||
|
away_temp = None
|
||||||
|
if not self._away:
|
||||||
|
self._prev_temp = self._target_temperature
|
||||||
|
if self._current_operation == STATE_HEAT:
|
||||||
|
away_temp = self._away_temps[0]
|
||||||
|
elif self._current_operation == STATE_COOL:
|
||||||
|
away_temp = self._away_temps[1]
|
||||||
|
self._away = True
|
||||||
|
self.set_temperature(temperature=away_temp)
|
||||||
|
|
||||||
|
def turn_away_mode_off(self):
|
||||||
|
"""Turn away off."""
|
||||||
|
self._away = False
|
||||||
|
self.set_temperature(temperature=self._prev_temp)
|
||||||
|
46
homeassistant/components/cover/tellduslive.py
Normal file
46
homeassistant/components/cover/tellduslive.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
Support for Tellstick covers using Tellstick Net.
|
||||||
|
|
||||||
|
This platform uses the Telldus Live online service.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/cover.tellduslive/
|
||||||
|
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.cover import CoverDevice
|
||||||
|
from homeassistant.components.tellduslive import TelldusLiveEntity
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup covers."""
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
add_devices(TelldusLiveCover(hass, cover) for cover in discovery_info)
|
||||||
|
|
||||||
|
|
||||||
|
class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
|
||||||
|
"""Representation of a cover."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
"""Return the current position of the cover."""
|
||||||
|
return self.device.is_down
|
||||||
|
|
||||||
|
def close_cover(self, **kwargs):
|
||||||
|
"""Close the cover."""
|
||||||
|
self.device.down()
|
||||||
|
self.changed()
|
||||||
|
|
||||||
|
def open_cover(self, **kwargs):
|
||||||
|
"""Open the cover."""
|
||||||
|
self.device.up()
|
||||||
|
self.changed()
|
||||||
|
|
||||||
|
def stop_cover(self, **kwargs):
|
||||||
|
"""Stop the cover."""
|
||||||
|
self.device.stop()
|
||||||
|
self.changed()
|
@ -282,7 +282,7 @@ class DeviceTracker(object):
|
|||||||
list(self.group.tracking) + [device.entity_id])
|
list(self.group.tracking) + [device.entity_id])
|
||||||
|
|
||||||
# lookup mac vendor string to be stored in config
|
# lookup mac vendor string to be stored in config
|
||||||
device.set_vendor_for_mac()
|
yield from device.set_vendor_for_mac()
|
||||||
|
|
||||||
# update known_devices.yaml
|
# update known_devices.yaml
|
||||||
self.hass.async_add_job(
|
self.hass.async_add_job(
|
||||||
@ -370,6 +370,7 @@ class Device(Entity):
|
|||||||
|
|
||||||
self.away_hide = hide_if_away
|
self.away_hide = hide_if_away
|
||||||
self.vendor = vendor
|
self.vendor = vendor
|
||||||
|
self._attributes = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -399,12 +400,13 @@ class Device(Entity):
|
|||||||
if self.battery:
|
if self.battery:
|
||||||
attr[ATTR_BATTERY] = self.battery
|
attr[ATTR_BATTERY] = self.battery
|
||||||
|
|
||||||
if self.attributes:
|
|
||||||
for key, value in self.attributes.items():
|
|
||||||
attr[key] = value
|
|
||||||
|
|
||||||
return attr
|
return attr
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return device state attributes."""
|
||||||
|
return self._attributes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hidden(self):
|
def hidden(self):
|
||||||
"""If device should be hidden."""
|
"""If device should be hidden."""
|
||||||
@ -419,8 +421,11 @@ class Device(Entity):
|
|||||||
self.host_name = host_name
|
self.host_name = host_name
|
||||||
self.location_name = location_name
|
self.location_name = location_name
|
||||||
self.gps_accuracy = gps_accuracy or 0
|
self.gps_accuracy = gps_accuracy or 0
|
||||||
self.battery = battery
|
if battery:
|
||||||
self.attributes = attributes
|
self.battery = battery
|
||||||
|
if attributes:
|
||||||
|
self._attributes.update(attributes)
|
||||||
|
|
||||||
self.gps = None
|
self.gps = None
|
||||||
|
|
||||||
if gps is not None:
|
if gps is not None:
|
||||||
|
@ -286,8 +286,10 @@ class AsusWrtDeviceScanner(object):
|
|||||||
|
|
||||||
# match mac addresses to IP addresses in ARP table
|
# match mac addresses to IP addresses in ARP table
|
||||||
for arp in result.arp:
|
for arp in result.arp:
|
||||||
if match.group('mac').lower() in arp.decode('utf-8'):
|
if match.group('mac').lower() in \
|
||||||
arp_match = _ARP_REGEX.search(arp.decode('utf-8'))
|
arp.decode('utf-8').lower():
|
||||||
|
arp_match = _ARP_REGEX.search(
|
||||||
|
arp.decode('utf-8').lower())
|
||||||
if not arp_match:
|
if not arp_match:
|
||||||
_LOGGER.warning('Could not parse arp row: %s', arp)
|
_LOGGER.warning('Could not parse arp row: %s', arp)
|
||||||
continue
|
continue
|
||||||
|
@ -64,9 +64,22 @@ class GPSLoggerView(HomeAssistantView):
|
|||||||
if 'battery' in data:
|
if 'battery' in data:
|
||||||
battery = float(data['battery'])
|
battery = float(data['battery'])
|
||||||
|
|
||||||
|
attrs = {}
|
||||||
|
if 'speed' in data:
|
||||||
|
attrs['speed'] = float(data['speed'])
|
||||||
|
if 'direction' in data:
|
||||||
|
attrs['direction'] = float(data['direction'])
|
||||||
|
if 'altitude' in data:
|
||||||
|
attrs['altitude'] = float(data['altitude'])
|
||||||
|
if 'provider' in data:
|
||||||
|
attrs['provider'] = data['provider']
|
||||||
|
if 'activity' in data:
|
||||||
|
attrs['activity'] = data['activity']
|
||||||
|
|
||||||
yield from hass.loop.run_in_executor(
|
yield from hass.loop.run_in_executor(
|
||||||
None, partial(self.see, dev_id=device,
|
None, partial(self.see, dev_id=device,
|
||||||
gps=gps_location, battery=battery,
|
gps=gps_location, battery=battery,
|
||||||
gps_accuracy=accuracy))
|
gps_accuracy=accuracy,
|
||||||
|
attributes=attrs))
|
||||||
|
|
||||||
return 'Setting location for {}'.format(device)
|
return 'Setting location for {}'.format(device)
|
||||||
|
@ -25,6 +25,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
CONF_EXCLUDE = 'exclude'
|
CONF_EXCLUDE = 'exclude'
|
||||||
# Interval in minutes to exclude devices from a scan while they are home
|
# Interval in minutes to exclude devices from a scan while they are home
|
||||||
CONF_HOME_INTERVAL = 'home_interval'
|
CONF_HOME_INTERVAL = 'home_interval'
|
||||||
|
CONF_OPTIONS = 'scan_options'
|
||||||
|
DEFAULT_OPTIONS = '-F --host-timeout 5s'
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||||
|
|
||||||
@ -33,7 +35,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.Required(CONF_HOSTS): cv.ensure_list,
|
vol.Required(CONF_HOSTS): cv.ensure_list,
|
||||||
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
|
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
|
||||||
vol.Optional(CONF_EXCLUDE, default=[]):
|
vol.Optional(CONF_EXCLUDE, default=[]):
|
||||||
vol.All(cv.ensure_list, vol.Length(min=1))
|
vol.All(cv.ensure_list, vol.Length(min=1)),
|
||||||
|
vol.Optional(CONF_OPTIONS, default=DEFAULT_OPTIONS):
|
||||||
|
cv.string
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -69,8 +73,9 @@ class NmapDeviceScanner(object):
|
|||||||
self.last_results = []
|
self.last_results = []
|
||||||
|
|
||||||
self.hosts = config[CONF_HOSTS]
|
self.hosts = config[CONF_HOSTS]
|
||||||
self.exclude = config.get(CONF_EXCLUDE, [])
|
self.exclude = config[CONF_EXCLUDE]
|
||||||
minutes = config[CONF_HOME_INTERVAL]
|
minutes = config[CONF_HOME_INTERVAL]
|
||||||
|
self._options = config[CONF_OPTIONS]
|
||||||
self.home_interval = timedelta(minutes=minutes)
|
self.home_interval = timedelta(minutes=minutes)
|
||||||
|
|
||||||
self.success_init = self._update_info()
|
self.success_init = self._update_info()
|
||||||
@ -103,7 +108,7 @@ class NmapDeviceScanner(object):
|
|||||||
from nmap import PortScanner, PortScannerError
|
from nmap import PortScanner, PortScannerError
|
||||||
scanner = PortScanner()
|
scanner = PortScanner()
|
||||||
|
|
||||||
options = '-F --host-timeout 5s '
|
options = self._options
|
||||||
|
|
||||||
if self.home_interval:
|
if self.home_interval:
|
||||||
boundary = dt_util.now() - self.home_interval
|
boundary = dt_util.now() - self.home_interval
|
||||||
|
@ -9,16 +9,20 @@ import urllib
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import homeassistant.loader as loader
|
||||||
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
|
||||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||||
|
|
||||||
# Unifi package doesn't list urllib3 as a requirement
|
# Unifi package doesn't list urllib3 as a requirement
|
||||||
REQUIREMENTS = ['urllib3', 'unifi==1.2.5']
|
REQUIREMENTS = ['urllib3', 'pyunifi==1.3']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
CONF_PORT = 'port'
|
CONF_PORT = 'port'
|
||||||
CONF_SITE_ID = 'site_id'
|
CONF_SITE_ID = 'site_id'
|
||||||
|
|
||||||
|
NOTIFICATION_ID = 'unifi_notification'
|
||||||
|
NOTIFICATION_TITLE = 'Unifi Device Tracker Setup'
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_HOST, default='localhost'): cv.string,
|
vol.Optional(CONF_HOST, default='localhost'): cv.string,
|
||||||
vol.Optional(CONF_SITE_ID, default='default'): cv.string,
|
vol.Optional(CONF_SITE_ID, default='default'): cv.string,
|
||||||
@ -30,7 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
|
|
||||||
def get_scanner(hass, config):
|
def get_scanner(hass, config):
|
||||||
"""Setup Unifi device_tracker."""
|
"""Setup Unifi device_tracker."""
|
||||||
from unifi.controller import Controller
|
from pyunifi.controller import Controller
|
||||||
|
|
||||||
host = config[DOMAIN].get(CONF_HOST)
|
host = config[DOMAIN].get(CONF_HOST)
|
||||||
username = config[DOMAIN].get(CONF_USERNAME)
|
username = config[DOMAIN].get(CONF_USERNAME)
|
||||||
@ -38,10 +42,18 @@ def get_scanner(hass, config):
|
|||||||
site_id = config[DOMAIN].get(CONF_SITE_ID)
|
site_id = config[DOMAIN].get(CONF_SITE_ID)
|
||||||
port = config[DOMAIN].get(CONF_PORT)
|
port = config[DOMAIN].get(CONF_PORT)
|
||||||
|
|
||||||
|
persistent_notification = loader.get_component('persistent_notification')
|
||||||
try:
|
try:
|
||||||
ctrl = Controller(host, username, password, port, 'v4', site_id)
|
ctrl = Controller(host, username, password, port, 'v4', site_id)
|
||||||
except urllib.error.HTTPError as ex:
|
except urllib.error.HTTPError as ex:
|
||||||
_LOGGER.error('Failed to connect to unifi: %s', ex)
|
_LOGGER.error('Failed to connect to Unifi: %s', ex)
|
||||||
|
persistent_notification.create(
|
||||||
|
hass, 'Failed to connect to Unifi. '
|
||||||
|
'Error: {}<br />'
|
||||||
|
'You will need to restart hass after fixing.'
|
||||||
|
''.format(ex),
|
||||||
|
title=NOTIFICATION_TITLE,
|
||||||
|
notification_id=NOTIFICATION_ID)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return UnifiScanner(ctrl)
|
return UnifiScanner(ctrl)
|
||||||
|
@ -14,7 +14,7 @@ import voluptuous as vol
|
|||||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||||
from homeassistant.helpers.discovery import load_platform, discover
|
from homeassistant.helpers.discovery import load_platform, discover
|
||||||
|
|
||||||
REQUIREMENTS = ['netdisco==0.7.7']
|
REQUIREMENTS = ['netdisco==0.8.1']
|
||||||
|
|
||||||
DOMAIN = 'discovery'
|
DOMAIN = 'discovery'
|
||||||
|
|
||||||
@ -36,6 +36,8 @@ SERVICE_HANDLERS = {
|
|||||||
'yamaha': ('media_player', 'yamaha'),
|
'yamaha': ('media_player', 'yamaha'),
|
||||||
'logitech_mediaserver': ('media_player', 'squeezebox'),
|
'logitech_mediaserver': ('media_player', 'squeezebox'),
|
||||||
'directv': ('media_player', 'directv'),
|
'directv': ('media_player', 'directv'),
|
||||||
|
'denonavr': ('media_player', 'denonavr'),
|
||||||
|
'samsung_tv': ('media_player', 'samsungtv'),
|
||||||
}
|
}
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
@ -6,12 +6,16 @@ from aiohttp import web
|
|||||||
|
|
||||||
from homeassistant import core
|
from homeassistant import core
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_SET,
|
||||||
STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||||
)
|
)
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.media_player import (
|
||||||
|
ATTR_MEDIA_VOLUME_LEVEL, ATTR_SUPPORTED_MEDIA_COMMANDS,
|
||||||
|
SUPPORT_VOLUME_SET,
|
||||||
|
)
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -65,8 +69,11 @@ class HueAllLightsStateView(HomeAssistantView):
|
|||||||
|
|
||||||
for entity in hass.states.async_all():
|
for entity in hass.states.async_all():
|
||||||
if self.config.is_entity_exposed(entity):
|
if self.config.is_entity_exposed(entity):
|
||||||
|
state, brightness = get_entity_state(self.config, entity)
|
||||||
|
|
||||||
number = self.config.entity_id_to_number(entity.entity_id)
|
number = self.config.entity_id_to_number(entity.entity_id)
|
||||||
json_response[number] = entity_to_json(entity)
|
json_response[number] = entity_to_json(
|
||||||
|
entity, state, brightness)
|
||||||
|
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
@ -97,16 +104,9 @@ class HueOneLightStateView(HomeAssistantView):
|
|||||||
_LOGGER.error('Entity not exposed: %s', entity_id)
|
_LOGGER.error('Entity not exposed: %s', entity_id)
|
||||||
return web.Response(text="Entity not exposed", status=404)
|
return web.Response(text="Entity not exposed", status=404)
|
||||||
|
|
||||||
cached_state = self.config.cached_states.get(entity_id, None)
|
state, brightness = get_entity_state(self.config, entity)
|
||||||
|
|
||||||
if cached_state is None:
|
json_response = entity_to_json(entity, state, brightness)
|
||||||
final_state = entity.state == STATE_ON
|
|
||||||
final_brightness = entity.attributes.get(
|
|
||||||
ATTR_BRIGHTNESS, 255 if final_state else 0)
|
|
||||||
else:
|
|
||||||
final_state, final_brightness = cached_state
|
|
||||||
|
|
||||||
json_response = entity_to_json(entity, final_state, final_brightness)
|
|
||||||
|
|
||||||
return self.json(json_response)
|
return self.json(json_response)
|
||||||
|
|
||||||
@ -158,14 +158,24 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||||||
|
|
||||||
result, brightness = parsed
|
result, brightness = parsed
|
||||||
|
|
||||||
|
# Choose general HA domain
|
||||||
|
domain = core.DOMAIN
|
||||||
|
|
||||||
# Convert the resulting "on" status into the service we need to call
|
# Convert the resulting "on" status into the service we need to call
|
||||||
service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
|
service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
|
||||||
|
|
||||||
# Construct what we need to send to the service
|
# Construct what we need to send to the service
|
||||||
data = {ATTR_ENTITY_ID: entity_id}
|
data = {ATTR_ENTITY_ID: entity_id}
|
||||||
|
|
||||||
|
# Make sure the entity actually supports brightness
|
||||||
|
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
|
||||||
|
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||||
|
if brightness is not None:
|
||||||
|
data[ATTR_BRIGHTNESS] = brightness
|
||||||
|
|
||||||
# If the requested entity is a script add some variables
|
# If the requested entity is a script add some variables
|
||||||
if entity.domain == "script":
|
elif entity.domain == "script":
|
||||||
data['variables'] = {
|
data['variables'] = {
|
||||||
'requested_state': STATE_ON if result else STATE_OFF
|
'requested_state': STATE_ON if result else STATE_OFF
|
||||||
}
|
}
|
||||||
@ -173,8 +183,16 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
data['variables']['requested_level'] = brightness
|
data['variables']['requested_level'] = brightness
|
||||||
|
|
||||||
elif brightness is not None:
|
# If the requested entity is a media player, convert to volume
|
||||||
data[ATTR_BRIGHTNESS] = brightness
|
elif entity.domain == "media_player":
|
||||||
|
media_commands = entity.attributes.get(
|
||||||
|
ATTR_SUPPORTED_MEDIA_COMMANDS, 0)
|
||||||
|
if media_commands & SUPPORT_VOLUME_SET == SUPPORT_VOLUME_SET:
|
||||||
|
if brightness is not None:
|
||||||
|
domain = entity.domain
|
||||||
|
service = SERVICE_VOLUME_SET
|
||||||
|
# Convert 0-100 to 0.0-1.0
|
||||||
|
data[ATTR_MEDIA_VOLUME_LEVEL] = brightness / 100.0
|
||||||
|
|
||||||
if entity.domain in config.off_maps_to_on_domains:
|
if entity.domain in config.off_maps_to_on_domains:
|
||||||
# Map the off command to on
|
# Map the off command to on
|
||||||
@ -187,9 +205,14 @@ class HueOneLightChangeView(HomeAssistantView):
|
|||||||
# as the actual requested command.
|
# as the actual requested command.
|
||||||
config.cached_states[entity_id] = (result, brightness)
|
config.cached_states[entity_id] = (result, brightness)
|
||||||
|
|
||||||
# Perform the requested action
|
# Separate call to turn on needed
|
||||||
yield from hass.services.async_call(core.DOMAIN, service, data,
|
if domain != core.DOMAIN:
|
||||||
blocking=True)
|
hass.async_add_job(hass.services.async_call(
|
||||||
|
core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id},
|
||||||
|
blocking=True))
|
||||||
|
|
||||||
|
hass.async_add_job(hass.services.async_call(
|
||||||
|
domain, service, data, blocking=True))
|
||||||
|
|
||||||
json_response = \
|
json_response = \
|
||||||
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
|
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
|
||||||
@ -219,23 +242,23 @@ def parse_hue_api_put_light_body(request_json, entity):
|
|||||||
result = False
|
result = False
|
||||||
|
|
||||||
if HUE_API_STATE_BRI in request_json:
|
if HUE_API_STATE_BRI in request_json:
|
||||||
|
try:
|
||||||
|
# Clamp brightness from 0 to 255
|
||||||
|
brightness = \
|
||||||
|
max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
# Make sure the entity actually supports brightness
|
# Make sure the entity actually supports brightness
|
||||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
|
||||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||||
try:
|
|
||||||
# Clamp brightness from 0 to 255
|
|
||||||
brightness = \
|
|
||||||
max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
report_brightness = True
|
report_brightness = True
|
||||||
result = (brightness > 0)
|
result = (brightness > 0)
|
||||||
elif entity.domain.lower() == "script":
|
|
||||||
# Convert 0-255 to 0-100
|
|
||||||
level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100
|
|
||||||
|
|
||||||
|
elif entity.domain == "script" or entity.domain == "media_player":
|
||||||
|
# Convert 0-255 to 0-100
|
||||||
|
level = brightness / 255 * 100
|
||||||
brightness = round(level)
|
brightness = round(level)
|
||||||
report_brightness = True
|
report_brightness = True
|
||||||
result = True
|
result = True
|
||||||
@ -243,16 +266,35 @@ def parse_hue_api_put_light_body(request_json, entity):
|
|||||||
return (result, brightness) if report_brightness else (result, None)
|
return (result, brightness) if report_brightness else (result, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_entity_state(config, entity):
|
||||||
|
"""Retrieve and convert state and brightness values for an entity."""
|
||||||
|
cached_state = config.cached_states.get(entity.entity_id, None)
|
||||||
|
|
||||||
|
if cached_state is None:
|
||||||
|
final_state = entity.state != STATE_OFF
|
||||||
|
final_brightness = entity.attributes.get(
|
||||||
|
ATTR_BRIGHTNESS, 255 if final_state else 0)
|
||||||
|
|
||||||
|
# Make sure the entity actually supports brightness
|
||||||
|
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
|
||||||
|
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif entity.domain == "media_player":
|
||||||
|
level = entity.attributes.get(
|
||||||
|
ATTR_MEDIA_VOLUME_LEVEL, 1.0 if final_state else 0.0)
|
||||||
|
# Convert 0.0-1.0 to 0-255
|
||||||
|
final_brightness = round(min(1.0, level) * 255)
|
||||||
|
else:
|
||||||
|
final_state, final_brightness = cached_state
|
||||||
|
|
||||||
|
return (final_state, final_brightness)
|
||||||
|
|
||||||
|
|
||||||
def entity_to_json(entity, is_on=None, brightness=None):
|
def entity_to_json(entity, is_on=None, brightness=None):
|
||||||
"""Convert an entity to its Hue bridge JSON representation."""
|
"""Convert an entity to its Hue bridge JSON representation."""
|
||||||
if is_on is None:
|
name = entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name)
|
||||||
is_on = entity.state == STATE_ON
|
|
||||||
|
|
||||||
if brightness is None:
|
|
||||||
brightness = 255 if is_on else 0
|
|
||||||
|
|
||||||
name = entity.attributes.get(
|
|
||||||
ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'state':
|
'state':
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||||
|
|
||||||
FINGERPRINTS = {
|
FINGERPRINTS = {
|
||||||
"core.js": "5dfb2d3e567fad37af0321d4b29265ed",
|
"core.js": "ad1ebcd0614c98a390d982087a7ca75c",
|
||||||
"frontend.html": "6a89b74ab2b76c7d28fad2aea9444ec2",
|
"frontend.html": "826ee6a4b39c939e31aa468b1ef618f9",
|
||||||
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
||||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||||
"panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e",
|
"panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e",
|
||||||
"panels/ha-panel-dev-info.html": "a9c07bf281fe9791fb15827ec1286825",
|
"panels/ha-panel-dev-info.html": "a9c07bf281fe9791fb15827ec1286825",
|
||||||
"panels/ha-panel-dev-service.html": "b3fe49532c5c03198fafb0c6ed58b76a",
|
"panels/ha-panel-dev-service.html": "ac74f7ce66fd7136d25c914ea12f4351",
|
||||||
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
|
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
|
||||||
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
|
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
|
||||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||||
"panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb",
|
"panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb",
|
||||||
"panels/ha-panel-map.html": "1bf6965b24d76db71a1871865cd4a3a2",
|
"panels/ha-panel-map.html": "3b0ca63286cbe80f27bd36dbc2434e89",
|
||||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||||
}
|
}
|
||||||
|
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 2652823d35b77411988751cc74820dcfc3a0e2ac
|
Subproject commit b5c3575cb5f284178e52d75db24c46131afb4cfa
|
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.
@ -29,11 +29,13 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
|||||||
|
|
||||||
CONF_ENTITIES = 'entities'
|
CONF_ENTITIES = 'entities'
|
||||||
CONF_VIEW = 'view'
|
CONF_VIEW = 'view'
|
||||||
|
CONF_CONTROL = 'control'
|
||||||
|
|
||||||
ATTR_AUTO = 'auto'
|
ATTR_AUTO = 'auto'
|
||||||
ATTR_ORDER = 'order'
|
ATTR_ORDER = 'order'
|
||||||
ATTR_VIEW = 'view'
|
ATTR_VIEW = 'view'
|
||||||
ATTR_VISIBLE = 'visible'
|
ATTR_VISIBLE = 'visible'
|
||||||
|
ATTR_CONTROL = 'control'
|
||||||
|
|
||||||
SERVICE_SET_VISIBILITY = 'set_visibility'
|
SERVICE_SET_VISIBILITY = 'set_visibility'
|
||||||
SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({
|
SET_VISIBILITY_SERVICE_SCHEMA = vol.Schema({
|
||||||
@ -61,6 +63,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
CONF_VIEW: cv.boolean,
|
CONF_VIEW: cv.boolean,
|
||||||
CONF_NAME: cv.string,
|
CONF_NAME: cv.string,
|
||||||
CONF_ICON: cv.icon,
|
CONF_ICON: cv.icon,
|
||||||
|
CONF_CONTROL: cv.string,
|
||||||
}, cv.match_all))
|
}, cv.match_all))
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
@ -206,11 +209,13 @@ def _async_process_config(hass, config, component):
|
|||||||
entity_ids = conf.get(CONF_ENTITIES) or []
|
entity_ids = conf.get(CONF_ENTITIES) or []
|
||||||
icon = conf.get(CONF_ICON)
|
icon = conf.get(CONF_ICON)
|
||||||
view = conf.get(CONF_VIEW)
|
view = conf.get(CONF_VIEW)
|
||||||
|
control = conf.get(CONF_CONTROL)
|
||||||
|
|
||||||
# Don't create tasks and await them all. The order is important as
|
# Don't create tasks and await them all. The order is important as
|
||||||
# groups get a number based on creation order.
|
# groups get a number based on creation order.
|
||||||
group = yield from Group.async_create_group(
|
group = yield from Group.async_create_group(
|
||||||
hass, name, entity_ids, icon=icon, view=view, object_id=object_id)
|
hass, name, entity_ids, icon=icon, view=view,
|
||||||
|
control=control, object_id=object_id)
|
||||||
groups.append(group)
|
groups.append(group)
|
||||||
|
|
||||||
if groups:
|
if groups:
|
||||||
@ -221,7 +226,7 @@ class Group(Entity):
|
|||||||
"""Track a group of entity ids."""
|
"""Track a group of entity ids."""
|
||||||
|
|
||||||
def __init__(self, hass, name, order=None, user_defined=True, icon=None,
|
def __init__(self, hass, name, order=None, user_defined=True, icon=None,
|
||||||
view=False):
|
view=False, control=None):
|
||||||
"""Initialize a group.
|
"""Initialize a group.
|
||||||
|
|
||||||
This Object has factory function for creation.
|
This Object has factory function for creation.
|
||||||
@ -239,20 +244,22 @@ class Group(Entity):
|
|||||||
self._assumed_state = False
|
self._assumed_state = False
|
||||||
self._async_unsub_state_changed = None
|
self._async_unsub_state_changed = None
|
||||||
self._visible = True
|
self._visible = True
|
||||||
|
self._control = control
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_group(hass, name, entity_ids=None, user_defined=True,
|
def create_group(hass, name, entity_ids=None, user_defined=True,
|
||||||
icon=None, view=False, object_id=None):
|
icon=None, view=False, control=None, object_id=None):
|
||||||
"""Initialize a group."""
|
"""Initialize a group."""
|
||||||
return run_coroutine_threadsafe(
|
return run_coroutine_threadsafe(
|
||||||
Group.async_create_group(hass, name, entity_ids, user_defined,
|
Group.async_create_group(hass, name, entity_ids, user_defined,
|
||||||
icon, view, object_id),
|
icon, view, control, object_id),
|
||||||
hass.loop).result()
|
hass.loop).result()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_create_group(hass, name, entity_ids=None, user_defined=True,
|
def async_create_group(hass, name, entity_ids=None, user_defined=True,
|
||||||
icon=None, view=False, object_id=None):
|
icon=None, view=False, control=None,
|
||||||
|
object_id=None):
|
||||||
"""Initialize a group.
|
"""Initialize a group.
|
||||||
|
|
||||||
This method must be run in the event loop.
|
This method must be run in the event loop.
|
||||||
@ -260,7 +267,8 @@ class Group(Entity):
|
|||||||
group = Group(
|
group = Group(
|
||||||
hass, name,
|
hass, name,
|
||||||
order=len(hass.states.async_entity_ids(DOMAIN)),
|
order=len(hass.states.async_entity_ids(DOMAIN)),
|
||||||
user_defined=user_defined, icon=icon, view=view)
|
user_defined=user_defined, icon=icon, view=view,
|
||||||
|
control=control)
|
||||||
|
|
||||||
group.entity_id = async_generate_entity_id(
|
group.entity_id = async_generate_entity_id(
|
||||||
ENTITY_ID_FORMAT, object_id or name, hass=hass)
|
ENTITY_ID_FORMAT, object_id or name, hass=hass)
|
||||||
@ -319,6 +327,8 @@ class Group(Entity):
|
|||||||
data[ATTR_AUTO] = True
|
data[ATTR_AUTO] = True
|
||||||
if self._view:
|
if self._view:
|
||||||
data[ATTR_VIEW] = True
|
data[ATTR_VIEW] = True
|
||||||
|
if self._control:
|
||||||
|
data[ATTR_CONTROL] = self._control
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -825,7 +825,7 @@ class HMDevice(Entity):
|
|||||||
if have_change:
|
if have_change:
|
||||||
_LOGGER.debug("%s update_ha_state after '%s'", self._name,
|
_LOGGER.debug("%s update_ha_state after '%s'", self._name,
|
||||||
attribute)
|
attribute)
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def _subscribe_homematic_events(self):
|
def _subscribe_homematic_events(self):
|
||||||
"""Subscribe all required events to handle job."""
|
"""Subscribe all required events to handle job."""
|
||||||
|
@ -288,10 +288,16 @@ class HomeAssistantWSGI(object):
|
|||||||
cors_added.add(route)
|
cors_added.add(route)
|
||||||
|
|
||||||
if self.ssl_certificate:
|
if self.ssl_certificate:
|
||||||
context = ssl.SSLContext(SSL_VERSION)
|
try:
|
||||||
context.options |= SSL_OPTS
|
context = ssl.SSLContext(SSL_VERSION)
|
||||||
context.set_ciphers(CIPHERS)
|
context.options |= SSL_OPTS
|
||||||
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
|
context.set_ciphers(CIPHERS)
|
||||||
|
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
|
||||||
|
except OSError as error:
|
||||||
|
_LOGGER.error("Could not read SSL certificate from %s: %s",
|
||||||
|
self.ssl_certificate, error)
|
||||||
|
context = None
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
context = None
|
context = None
|
||||||
|
|
||||||
@ -305,18 +311,24 @@ class HomeAssistantWSGI(object):
|
|||||||
|
|
||||||
self._handler = self.app.make_handler()
|
self._handler = self.app.make_handler()
|
||||||
|
|
||||||
self.server = yield from self.hass.loop.create_server(
|
try:
|
||||||
self._handler, self.server_host, self.server_port, ssl=context)
|
self.server = yield from self.hass.loop.create_server(
|
||||||
|
self._handler, self.server_host, self.server_port, ssl=context)
|
||||||
|
except OSError as error:
|
||||||
|
_LOGGER.error("Failed to create HTTP server at port %d: %s",
|
||||||
|
self.server_port, error)
|
||||||
|
|
||||||
self.app._frozen = False # pylint: disable=protected-access
|
self.app._frozen = False # pylint: disable=protected-access
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop the wsgi server."""
|
"""Stop the wsgi server."""
|
||||||
self.server.close()
|
if self.server:
|
||||||
yield from self.server.wait_closed()
|
self.server.close()
|
||||||
|
yield from self.server.wait_closed()
|
||||||
yield from self.app.shutdown()
|
yield from self.app.shutdown()
|
||||||
yield from self._handler.finish_connections(60.0)
|
if self._handler:
|
||||||
|
yield from self._handler.finish_connections(60.0)
|
||||||
yield from self.app.cleanup()
|
yield from self.app.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
@ -95,7 +95,8 @@ def async_setup(hass, config):
|
|||||||
attr = 'async_toggle'
|
attr = 'async_toggle'
|
||||||
|
|
||||||
tasks = [getattr(input_b, attr)() for input_b in target_inputs]
|
tasks = [getattr(input_b, attr)() for input_b in target_inputs]
|
||||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
if tasks:
|
||||||
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_TURN_OFF, async_handler_service, schema=SERVICE_SCHEMA)
|
DOMAIN, SERVICE_TURN_OFF, async_handler_service, schema=SERVICE_SCHEMA)
|
||||||
|
@ -113,7 +113,8 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
tasks = [input_select.async_select_option(call.data[ATTR_OPTION])
|
tasks = [input_select.async_select_option(call.data[ATTR_OPTION])
|
||||||
for input_select in target_inputs]
|
for input_select in target_inputs]
|
||||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
if tasks:
|
||||||
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SELECT_OPTION, async_select_option_service,
|
DOMAIN, SERVICE_SELECT_OPTION, async_select_option_service,
|
||||||
@ -126,7 +127,8 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
tasks = [input_select.async_offset_index(1)
|
tasks = [input_select.async_offset_index(1)
|
||||||
for input_select in target_inputs]
|
for input_select in target_inputs]
|
||||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
if tasks:
|
||||||
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SELECT_NEXT, async_select_next_service,
|
DOMAIN, SERVICE_SELECT_NEXT, async_select_next_service,
|
||||||
@ -139,7 +141,8 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
tasks = [input_select.async_offset_index(-1)
|
tasks = [input_select.async_offset_index(-1)
|
||||||
for input_select in target_inputs]
|
for input_select in target_inputs]
|
||||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
if tasks:
|
||||||
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service,
|
DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service,
|
||||||
|
@ -105,7 +105,8 @@ def async_setup(hass, config):
|
|||||||
|
|
||||||
tasks = [input_slider.async_select_value(call.data[ATTR_VALUE])
|
tasks = [input_slider.async_select_value(call.data[ATTR_VALUE])
|
||||||
for input_slider in target_inputs]
|
for input_slider in target_inputs]
|
||||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
if tasks:
|
||||||
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SELECT_VALUE, async_select_value_service,
|
DOMAIN, SERVICE_SELECT_VALUE, async_select_value_service,
|
||||||
|
@ -17,8 +17,8 @@ from homeassistant.components.light import (
|
|||||||
PLATFORM_SCHEMA)
|
PLATFORM_SCHEMA)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.9.zip'
|
REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.10.zip'
|
||||||
'#flux_led==0.9']
|
'#flux_led==0.10']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -90,6 +90,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
if discovery_info is not None:
|
if discovery_info is not None:
|
||||||
host = urlparse(discovery_info[1]).hostname
|
host = urlparse(discovery_info[1]).hostname
|
||||||
|
|
||||||
|
if "HASS Bridge" in discovery_info[0]:
|
||||||
|
_LOGGER.info('Emulated hue found, will not add')
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
host = config.get(CONF_HOST, None)
|
host = config.get(CONF_HOST, None)
|
||||||
|
|
||||||
@ -138,10 +142,14 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
|
|||||||
configurator.request_done(request_id)
|
configurator.request_done(request_id)
|
||||||
|
|
||||||
lights = {}
|
lights = {}
|
||||||
|
lightgroups = {}
|
||||||
|
skip_groups = False
|
||||||
|
|
||||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||||
def update_lights():
|
def update_lights():
|
||||||
"""Update the Hue light objects with latest info from the bridge."""
|
"""Update the Hue light objects with latest info from the bridge."""
|
||||||
|
nonlocal skip_groups
|
||||||
|
|
||||||
try:
|
try:
|
||||||
api = bridge.get_api()
|
api = bridge.get_api()
|
||||||
except socket.error:
|
except socket.error:
|
||||||
@ -149,9 +157,18 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
|
|||||||
_LOGGER.exception("Cannot reach the bridge")
|
_LOGGER.exception("Cannot reach the bridge")
|
||||||
return
|
return
|
||||||
|
|
||||||
api_states = api.get('lights')
|
api_lights = api.get('lights')
|
||||||
|
|
||||||
if not isinstance(api_states, dict):
|
if not isinstance(api_lights, dict):
|
||||||
|
_LOGGER.error("Got unexpected result from Hue API")
|
||||||
|
return
|
||||||
|
|
||||||
|
if skip_groups:
|
||||||
|
api_groups = {}
|
||||||
|
else:
|
||||||
|
api_groups = api.get('groups')
|
||||||
|
|
||||||
|
if not isinstance(api_groups, dict):
|
||||||
_LOGGER.error("Got unexpected result from Hue API")
|
_LOGGER.error("Got unexpected result from Hue API")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -163,7 +180,7 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
|
|||||||
else:
|
else:
|
||||||
bridge_type = 'hue'
|
bridge_type = 'hue'
|
||||||
|
|
||||||
for light_id, info in api_states.items():
|
for light_id, info in api_lights.items():
|
||||||
if light_id not in lights:
|
if light_id not in lights:
|
||||||
lights[light_id] = HueLight(int(light_id), info,
|
lights[light_id] = HueLight(int(light_id), info,
|
||||||
bridge, update_lights,
|
bridge, update_lights,
|
||||||
@ -171,6 +188,23 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable):
|
|||||||
new_lights.append(lights[light_id])
|
new_lights.append(lights[light_id])
|
||||||
else:
|
else:
|
||||||
lights[light_id].info = info
|
lights[light_id].info = info
|
||||||
|
lights[light_id].schedule_update_ha_state()
|
||||||
|
|
||||||
|
for lightgroup_id, info in api_groups.items():
|
||||||
|
if 'state' not in info:
|
||||||
|
_LOGGER.warning('Group info does not contain state. '
|
||||||
|
'Please update your hub.')
|
||||||
|
skip_groups = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if lightgroup_id not in lightgroups:
|
||||||
|
lightgroups[lightgroup_id] = HueLight(
|
||||||
|
int(lightgroup_id), info, bridge, update_lights,
|
||||||
|
bridge_type, allow_unreachable, True)
|
||||||
|
new_lights.append(lightgroups[lightgroup_id])
|
||||||
|
else:
|
||||||
|
lightgroups[lightgroup_id].info = info
|
||||||
|
lightgroups[lightgroup_id].schedule_update_ha_state()
|
||||||
|
|
||||||
if new_lights:
|
if new_lights:
|
||||||
add_devices(new_lights)
|
add_devices(new_lights)
|
||||||
@ -225,15 +259,20 @@ class HueLight(Light):
|
|||||||
"""Representation of a Hue light."""
|
"""Representation of a Hue light."""
|
||||||
|
|
||||||
def __init__(self, light_id, info, bridge, update_lights,
|
def __init__(self, light_id, info, bridge, update_lights,
|
||||||
bridge_type, allow_unreachable):
|
bridge_type, allow_unreachable, is_group=False):
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
self.light_id = light_id
|
self.light_id = light_id
|
||||||
self.info = info
|
self.info = info
|
||||||
self.bridge = bridge
|
self.bridge = bridge
|
||||||
self.update_lights = update_lights
|
self.update_lights = update_lights
|
||||||
self.bridge_type = bridge_type
|
self.bridge_type = bridge_type
|
||||||
|
|
||||||
self.allow_unreachable = allow_unreachable
|
self.allow_unreachable = allow_unreachable
|
||||||
|
self.is_group = is_group
|
||||||
|
|
||||||
|
if is_group:
|
||||||
|
self._command_func = self.bridge.set_group
|
||||||
|
else:
|
||||||
|
self._command_func = self.bridge.set_light
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
@ -243,33 +282,44 @@ class HueLight(Light):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the mame of the Hue light."""
|
"""Return the name of the Hue light."""
|
||||||
return self.info.get('name', DEVICE_DEFAULT_NAME)
|
return self.info.get('name', DEVICE_DEFAULT_NAME)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brightness(self):
|
def brightness(self):
|
||||||
"""Return the brightness of this light between 0..255."""
|
"""Return the brightness of this light between 0..255."""
|
||||||
return self.info['state'].get('bri')
|
if self.is_group:
|
||||||
|
return self.info['action'].get('bri')
|
||||||
|
else:
|
||||||
|
return self.info['state'].get('bri')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def xy_color(self):
|
def xy_color(self):
|
||||||
"""Return the XY color value."""
|
"""Return the XY color value."""
|
||||||
return self.info['state'].get('xy')
|
if self.is_group:
|
||||||
|
return self.info['action'].get('xy')
|
||||||
|
else:
|
||||||
|
return self.info['state'].get('xy')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def color_temp(self):
|
def color_temp(self):
|
||||||
"""Return the CT color value."""
|
"""Return the CT color value."""
|
||||||
return self.info['state'].get('ct')
|
if self.is_group:
|
||||||
|
return self.info['action'].get('ct')
|
||||||
|
else:
|
||||||
|
return self.info['state'].get('ct')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if device is on."""
|
"""Return true if device is on."""
|
||||||
self.update_lights()
|
if self.is_group:
|
||||||
|
return self.info['state']['any_on']
|
||||||
if self.allow_unreachable:
|
|
||||||
return self.info['state']['on']
|
|
||||||
else:
|
else:
|
||||||
return self.info['state']['reachable'] and self.info['state']['on']
|
if self.allow_unreachable:
|
||||||
|
return self.info['state']['on']
|
||||||
|
else:
|
||||||
|
return self.info['state']['reachable'] and \
|
||||||
|
self.info['state']['on']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
@ -318,7 +368,7 @@ class HueLight(Light):
|
|||||||
elif self.bridge_type == 'hue':
|
elif self.bridge_type == 'hue':
|
||||||
command['effect'] = 'none'
|
command['effect'] = 'none'
|
||||||
|
|
||||||
self.bridge.set_light(self.light_id, command)
|
self._command_func(self.light_id, command)
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Turn the specified or all lights off."""
|
"""Turn the specified or all lights off."""
|
||||||
@ -340,7 +390,7 @@ class HueLight(Light):
|
|||||||
elif self.bridge_type == 'hue':
|
elif self.bridge_type == 'hue':
|
||||||
command['alert'] = 'none'
|
command['alert'] = 'none'
|
||||||
|
|
||||||
self.bridge.set_light(self.light_id, command)
|
self._command_func(self.light_id, command)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Synchronize state with bridge."""
|
"""Synchronize state with bridge."""
|
||||||
|
63
homeassistant/components/light/tellduslive.py
Normal file
63
homeassistant/components/light/tellduslive.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
Support for Tellstick switches using Tellstick Net.
|
||||||
|
|
||||||
|
This platform uses the Telldus Live online service.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/light.tellduslive/
|
||||||
|
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.light import (
|
||||||
|
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
|
||||||
|
from homeassistant.components.tellduslive import TelldusLiveEntity
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup lights."""
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
add_devices(TelldusLiveLight(hass, light) for light in discovery_info)
|
||||||
|
|
||||||
|
|
||||||
|
class TelldusLiveLight(TelldusLiveEntity, Light):
|
||||||
|
"""Representation of a light."""
|
||||||
|
|
||||||
|
def __init__(self, hass, device_id):
|
||||||
|
"""Initialize the light."""
|
||||||
|
super().__init__(hass, device_id)
|
||||||
|
self._last_brightness = self.brightness
|
||||||
|
|
||||||
|
def changed(self):
|
||||||
|
"""A property of the device might have changed."""
|
||||||
|
self._last_brightness = self.brightness
|
||||||
|
super().changed()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Return the brightness of this light between 0..255."""
|
||||||
|
return self.device.dim_level
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
return SUPPORT_BRIGHTNESS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if light is on."""
|
||||||
|
return self.device.is_on
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Turn the light on."""
|
||||||
|
brightness = kwargs.get(ATTR_BRIGHTNESS, self._last_brightness)
|
||||||
|
self.device.dim(level=brightness)
|
||||||
|
self.changed()
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Turn the light off."""
|
||||||
|
self.device.turn_off()
|
||||||
|
self.changed()
|
@ -29,16 +29,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG,
|
signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG,
|
||||||
DEFAULT_SIGNAL_REPETITIONS)
|
DEFAULT_SIGNAL_REPETITIONS)
|
||||||
|
|
||||||
add_devices(TellstickLight(tellcore_id, signal_repetitions)
|
add_devices(TellstickLight(tellcore_id, hass.data['tellcore_registry'],
|
||||||
|
signal_repetitions)
|
||||||
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES])
|
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES])
|
||||||
|
|
||||||
|
|
||||||
class TellstickLight(TellstickDevice, Light):
|
class TellstickLight(TellstickDevice, Light):
|
||||||
"""Representation of a Tellstick light."""
|
"""Representation of a Tellstick light."""
|
||||||
|
|
||||||
def __init__(self, tellcore_id, signal_repetitions):
|
def __init__(self, tellcore_id, tellcore_registry, signal_repetitions):
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
super().__init__(tellcore_id, signal_repetitions)
|
super().__init__(tellcore_id, tellcore_registry, signal_repetitions)
|
||||||
|
|
||||||
self._brightness = 255
|
self._brightness = 255
|
||||||
|
|
||||||
@ -71,7 +72,11 @@ class TellstickLight(TellstickDevice, Light):
|
|||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
self._brightness = brightness
|
self._brightness = brightness
|
||||||
|
|
||||||
self._state = (self._brightness > 0)
|
# _brightness is not defined when called from super
|
||||||
|
try:
|
||||||
|
self._state = (self._brightness > 0)
|
||||||
|
except AttributeError:
|
||||||
|
self._state = True
|
||||||
else:
|
else:
|
||||||
self._state = False
|
self._state = False
|
||||||
|
|
||||||
|
@ -162,7 +162,7 @@ MEDIA_PLAYER_MEDIA_SEEK_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
|||||||
MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||||
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
|
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
|
||||||
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
|
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
|
||||||
ATTR_MEDIA_ENQUEUE: cv.boolean,
|
vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
|
||||||
|
161
homeassistant/components/media_player/aquostv.py
Normal file
161
homeassistant/components/media_player/aquostv.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
"""
|
||||||
|
Support for interface with an Aquos TV.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/media_player.aquostv/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import (
|
||||||
|
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
||||||
|
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||||
|
SUPPORT_VOLUME_SET, MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN,
|
||||||
|
CONF_PORT, CONF_USERNAME, CONF_PASSWORD)
|
||||||
|
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
REQUIREMENTS = ['sharp-aquos-rc==0.2']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'Sharp Aquos TV'
|
||||||
|
DEFAULT_PORT = 10002
|
||||||
|
DEFAULT_USERNAME = 'admin'
|
||||||
|
DEFAULT_PASSWORD = 'password'
|
||||||
|
|
||||||
|
SUPPORT_SHARPTV = SUPPORT_VOLUME_STEP | \
|
||||||
|
SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||||
|
SUPPORT_TURN_OFF | SUPPORT_TURN_ON
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
|
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||||
|
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the Sharp Aquos TV platform."""
|
||||||
|
import sharp_aquos_rc
|
||||||
|
|
||||||
|
name = config.get(CONF_NAME)
|
||||||
|
port = config.get(CONF_PORT)
|
||||||
|
username = config.get(CONF_USERNAME)
|
||||||
|
password = config.get(CONF_PASSWORD)
|
||||||
|
|
||||||
|
if discovery_info:
|
||||||
|
_LOGGER.debug('%s', discovery_info)
|
||||||
|
vals = discovery_info.split(':')
|
||||||
|
if len(vals) > 1:
|
||||||
|
port = vals[1]
|
||||||
|
|
||||||
|
host = vals[0]
|
||||||
|
remote = sharp_aquos_rc.TV(host,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
password)
|
||||||
|
add_devices([SharpAquosTVDevice(name, remote)])
|
||||||
|
return True
|
||||||
|
|
||||||
|
host = config.get(CONF_HOST)
|
||||||
|
remote = sharp_aquos_rc.TV(host,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
password)
|
||||||
|
|
||||||
|
add_devices([SharpAquosTVDevice(name, remote)])
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
class SharpAquosTVDevice(MediaPlayerDevice):
|
||||||
|
"""Representation of a Aquos TV."""
|
||||||
|
|
||||||
|
# pylint: disable=too-many-public-methods
|
||||||
|
def __init__(self, name, remote):
|
||||||
|
"""Initialize the aquos device."""
|
||||||
|
# Save a reference to the imported class
|
||||||
|
self._name = name
|
||||||
|
# Assume that the TV is not muted
|
||||||
|
self._muted = False
|
||||||
|
# Assume that the TV is in Play mode
|
||||||
|
self._playing = True
|
||||||
|
self._state = STATE_UNKNOWN
|
||||||
|
self._remote = remote
|
||||||
|
self._volume = 0
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Retrieve the latest data."""
|
||||||
|
try:
|
||||||
|
if self._remote.power() == 1:
|
||||||
|
self._state = STATE_ON
|
||||||
|
else:
|
||||||
|
self._state = STATE_OFF
|
||||||
|
|
||||||
|
# Set TV to be able to remotely power on
|
||||||
|
# self._remote.power_on_command_settings(2)
|
||||||
|
if self._remote.mute() == 2:
|
||||||
|
self._muted = False
|
||||||
|
else:
|
||||||
|
self._muted = True
|
||||||
|
self._volume = self._remote.volume() / 60
|
||||||
|
except OSError:
|
||||||
|
self._state = STATE_OFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume_level(self):
|
||||||
|
"""Volume level of the media player (0..1)."""
|
||||||
|
return self._volume
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_volume_muted(self):
|
||||||
|
"""Boolean if volume is currently muted."""
|
||||||
|
return self._muted
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_media_commands(self):
|
||||||
|
"""Flag of media commands that are supported."""
|
||||||
|
return SUPPORT_SHARPTV
|
||||||
|
|
||||||
|
def turn_off(self):
|
||||||
|
"""Turn off tvplayer."""
|
||||||
|
self._remote.power(0)
|
||||||
|
|
||||||
|
def volume_up(self):
|
||||||
|
"""Volume up the media player."""
|
||||||
|
self._remote.volume(int(self._volume * 60) + 2)
|
||||||
|
|
||||||
|
def volume_down(self):
|
||||||
|
"""Volume down media player."""
|
||||||
|
self._remote.volume(int(self._volume * 60) - 2)
|
||||||
|
|
||||||
|
def set_volume_level(self, level):
|
||||||
|
"""Set Volume media player."""
|
||||||
|
self._remote.volume(int(level * 60))
|
||||||
|
|
||||||
|
def mute_volume(self, mute):
|
||||||
|
"""Send mute command."""
|
||||||
|
self._remote.mute(0)
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
"""Turn the media player on."""
|
||||||
|
self._remote.power(1)
|
@ -13,7 +13,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_ON,
|
||||||
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||||
SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice,
|
SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice,
|
||||||
PLATFORM_SCHEMA)
|
PLATFORM_SCHEMA)
|
||||||
@ -40,7 +40,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
||||||
SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
|
SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
|
||||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||||
SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
|
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
|
||||||
|
SUPPORT_SELECT_SOURCE
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
@ -18,6 +18,7 @@ from homeassistant.const import (
|
|||||||
CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
||||||
STATE_UNKNOWN)
|
STATE_UNKNOWN)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['pychromecast==0.7.6']
|
REQUIREMENTS = ['pychromecast==0.7.6']
|
||||||
|
|
||||||
@ -105,6 +106,7 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
self.cast_status = self.cast.status
|
self.cast_status = self.cast.status
|
||||||
self.media_status = self.cast.media_controller.status
|
self.media_status = self.cast.media_controller.status
|
||||||
|
self.media_status_received = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
@ -231,6 +233,30 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
"""Flag of media commands that are supported."""
|
"""Flag of media commands that are supported."""
|
||||||
return SUPPORT_CAST
|
return SUPPORT_CAST
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position(self):
|
||||||
|
"""Position of current playing media in seconds."""
|
||||||
|
if self.media_status is None or self.media_status_received is None or \
|
||||||
|
not (self.media_status.player_is_playing or
|
||||||
|
self.media_status.player_is_idle):
|
||||||
|
return None
|
||||||
|
|
||||||
|
position = self.media_status.current_time
|
||||||
|
|
||||||
|
if self.media_status.player_is_playing:
|
||||||
|
position += (dt_util.utcnow() -
|
||||||
|
self.media_status_received).total_seconds()
|
||||||
|
|
||||||
|
return position
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position_updated_at(self):
|
||||||
|
"""When was the position of the current playing media valid.
|
||||||
|
|
||||||
|
Returns value from homeassistant.util.dt.utcnow().
|
||||||
|
"""
|
||||||
|
return self.media_status_received
|
||||||
|
|
||||||
def turn_on(self):
|
def turn_on(self):
|
||||||
"""Turn on the ChromeCast."""
|
"""Turn on the ChromeCast."""
|
||||||
# The only way we can turn the Chromecast is on is by launching an app
|
# The only way we can turn the Chromecast is on is by launching an app
|
||||||
@ -292,4 +318,5 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
def new_media_status(self, status):
|
def new_media_status(self, status):
|
||||||
"""Called when a new media status is received."""
|
"""Called when a new media status is received."""
|
||||||
self.media_status = status
|
self.media_status = status
|
||||||
|
self.media_status_received = dt_util.utcnow()
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
@ -10,6 +10,7 @@ from homeassistant.components.media_player import (
|
|||||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||||
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, MediaPlayerDevice)
|
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, MediaPlayerDevice)
|
||||||
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
|
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
@ -18,8 +19,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
add_devices([
|
add_devices([
|
||||||
DemoYoutubePlayer(
|
DemoYoutubePlayer(
|
||||||
'Living Room', 'eyU3bRy2x44',
|
'Living Room', 'eyU3bRy2x44',
|
||||||
'♥♥ The Best Fireplace Video (3 hours)'),
|
'♥♥ The Best Fireplace Video (3 hours)', 300),
|
||||||
DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours'),
|
DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours',
|
||||||
|
360000),
|
||||||
DemoMusicPlayer(), DemoTVShowPlayer(),
|
DemoMusicPlayer(), DemoTVShowPlayer(),
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -78,32 +80,32 @@ class AbstractDemoPlayer(MediaPlayerDevice):
|
|||||||
def turn_on(self):
|
def turn_on(self):
|
||||||
"""Turn the media player on."""
|
"""Turn the media player on."""
|
||||||
self._player_state = STATE_PLAYING
|
self._player_state = STATE_PLAYING
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn the media player off."""
|
"""Turn the media player off."""
|
||||||
self._player_state = STATE_OFF
|
self._player_state = STATE_OFF
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def mute_volume(self, mute):
|
def mute_volume(self, mute):
|
||||||
"""Mute the volume."""
|
"""Mute the volume."""
|
||||||
self._volume_muted = mute
|
self._volume_muted = mute
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def set_volume_level(self, volume):
|
def set_volume_level(self, volume):
|
||||||
"""Set the volume level, range 0..1."""
|
"""Set the volume level, range 0..1."""
|
||||||
self._volume_level = volume
|
self._volume_level = volume
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def media_play(self):
|
def media_play(self):
|
||||||
"""Send play command."""
|
"""Send play command."""
|
||||||
self._player_state = STATE_PLAYING
|
self._player_state = STATE_PLAYING
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def media_pause(self):
|
def media_pause(self):
|
||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
self._player_state = STATE_PAUSED
|
self._player_state = STATE_PAUSED
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class DemoYoutubePlayer(AbstractDemoPlayer):
|
class DemoYoutubePlayer(AbstractDemoPlayer):
|
||||||
@ -111,11 +113,14 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
|
|||||||
|
|
||||||
# We only implement the methods that we support
|
# We only implement the methods that we support
|
||||||
|
|
||||||
def __init__(self, name, youtube_id=None, media_title=None):
|
def __init__(self, name, youtube_id=None, media_title=None, duration=360):
|
||||||
"""Initialize the demo device."""
|
"""Initialize the demo device."""
|
||||||
super().__init__(name)
|
super().__init__(name)
|
||||||
self.youtube_id = youtube_id
|
self.youtube_id = youtube_id
|
||||||
self._media_title = media_title
|
self._media_title = media_title
|
||||||
|
self._duration = duration
|
||||||
|
self._progress = int(duration * .15)
|
||||||
|
self._progress_updated_at = dt_util.utcnow()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_content_id(self):
|
def media_content_id(self):
|
||||||
@ -130,7 +135,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
|
|||||||
@property
|
@property
|
||||||
def media_duration(self):
|
def media_duration(self):
|
||||||
"""Return the duration of current playing media in seconds."""
|
"""Return the duration of current playing media in seconds."""
|
||||||
return 360
|
return self._duration
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_image_url(self):
|
def media_image_url(self):
|
||||||
@ -152,10 +157,39 @@ class DemoYoutubePlayer(AbstractDemoPlayer):
|
|||||||
"""Flag of media commands that are supported."""
|
"""Flag of media commands that are supported."""
|
||||||
return YOUTUBE_PLAYER_SUPPORT
|
return YOUTUBE_PLAYER_SUPPORT
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position(self):
|
||||||
|
"""Position of current playing media in seconds."""
|
||||||
|
if self._progress is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
position = self._progress
|
||||||
|
|
||||||
|
if self._player_state == STATE_PLAYING:
|
||||||
|
position += (dt_util.utcnow() -
|
||||||
|
self._progress_updated_at).total_seconds()
|
||||||
|
|
||||||
|
return position
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position_updated_at(self):
|
||||||
|
"""When was the position of the current playing media valid.
|
||||||
|
|
||||||
|
Returns value from homeassistant.util.dt.utcnow().
|
||||||
|
"""
|
||||||
|
if self._player_state == STATE_PLAYING:
|
||||||
|
return self._progress_updated_at
|
||||||
|
|
||||||
def play_media(self, media_type, media_id, **kwargs):
|
def play_media(self, media_type, media_id, **kwargs):
|
||||||
"""Play a piece of media."""
|
"""Play a piece of media."""
|
||||||
self.youtube_id = media_id
|
self.youtube_id = media_id
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
def media_pause(self):
|
||||||
|
"""Send pause command."""
|
||||||
|
self._progress = self.media_position
|
||||||
|
self._progress_updated_at = dt_util.utcnow()
|
||||||
|
super().media_pause()
|
||||||
|
|
||||||
|
|
||||||
class DemoMusicPlayer(AbstractDemoPlayer):
|
class DemoMusicPlayer(AbstractDemoPlayer):
|
||||||
@ -249,20 +283,20 @@ class DemoMusicPlayer(AbstractDemoPlayer):
|
|||||||
"""Send previous track command."""
|
"""Send previous track command."""
|
||||||
if self._cur_track > 0:
|
if self._cur_track > 0:
|
||||||
self._cur_track -= 1
|
self._cur_track -= 1
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def media_next_track(self):
|
def media_next_track(self):
|
||||||
"""Send next track command."""
|
"""Send next track command."""
|
||||||
if self._cur_track < len(self.tracks) - 1:
|
if self._cur_track < len(self.tracks) - 1:
|
||||||
self._cur_track += 1
|
self._cur_track += 1
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def clear_playlist(self):
|
def clear_playlist(self):
|
||||||
"""Clear players playlist."""
|
"""Clear players playlist."""
|
||||||
self.tracks = []
|
self.tracks = []
|
||||||
self._cur_track = 0
|
self._cur_track = 0
|
||||||
self._player_state = STATE_OFF
|
self._player_state = STATE_OFF
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class DemoTVShowPlayer(AbstractDemoPlayer):
|
class DemoTVShowPlayer(AbstractDemoPlayer):
|
||||||
@ -344,15 +378,15 @@ class DemoTVShowPlayer(AbstractDemoPlayer):
|
|||||||
"""Send previous track command."""
|
"""Send previous track command."""
|
||||||
if self._cur_episode > 1:
|
if self._cur_episode > 1:
|
||||||
self._cur_episode -= 1
|
self._cur_episode -= 1
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def media_next_track(self):
|
def media_next_track(self):
|
||||||
"""Send next track command."""
|
"""Send next track command."""
|
||||||
if self._cur_episode < self._episode_count:
|
if self._cur_episode < self._episode_count:
|
||||||
self._cur_episode += 1
|
self._cur_episode += 1
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def select_source(self, source):
|
def select_source(self, source):
|
||||||
"""Set the input source."""
|
"""Set the input source."""
|
||||||
self._source = source
|
self._source = source
|
||||||
self.update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
@ -13,7 +13,7 @@ from homeassistant.components.media_player import (
|
|||||||
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_SELECT_SOURCE,
|
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_SELECT_SOURCE,
|
||||||
SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF,
|
SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF,
|
||||||
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||||
MediaPlayerDevice)
|
SUPPORT_STOP, MediaPlayerDevice)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
|
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -22,16 +22,32 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
DEFAULT_NAME = 'Music station'
|
DEFAULT_NAME = 'Music station'
|
||||||
|
|
||||||
SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \
|
SUPPORT_DENON = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||||
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE \
|
||||||
SUPPORT_SELECT_SOURCE | SUPPORT_NEXT_TRACK | \
|
|
||||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
SUPPORT_MEDIA_MODES = SUPPORT_PAUSE | SUPPORT_STOP | \
|
||||||
|
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
NORMAL_INPUTS = {'Cd': 'CD', 'Dvd': 'DVD', 'Blue ray': 'BD', 'TV': 'TV',
|
||||||
|
'Satelite / Cable': 'SAT/CBL', 'Game': 'GAME',
|
||||||
|
'Game2': 'GAME2', 'Video Aux': 'V.AUX', 'Dock': 'DOCK'}
|
||||||
|
|
||||||
|
MEDIA_MODES = {'Tuner': 'TUNER', 'Media server': 'SERVER',
|
||||||
|
'Ipod dock': 'IPOD', 'Net/USB': 'NET/USB',
|
||||||
|
'Rapsody': 'RHAPSODY', 'Napster': 'NAPSTER',
|
||||||
|
'Pandora': 'PANDORA', 'LastFM': 'LASTFM',
|
||||||
|
'Flickr': 'FLICKR', 'Favorites': 'FAVORITES',
|
||||||
|
'Internet Radio': 'IRADIO', 'USB/IPOD': 'USB/IPOD'}
|
||||||
|
|
||||||
|
# Sub-modes of 'NET/USB'
|
||||||
|
# {'USB': 'USB', 'iPod Direct': 'IPD', 'Internet Radio': 'IRP',
|
||||||
|
# 'Favorites': 'FVP'}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Denon platform."""
|
"""Setup the Denon platform."""
|
||||||
@ -53,14 +69,39 @@ class DenonDevice(MediaPlayerDevice):
|
|||||||
self._host = host
|
self._host = host
|
||||||
self._pwstate = 'PWSTANDBY'
|
self._pwstate = 'PWSTANDBY'
|
||||||
self._volume = 0
|
self._volume = 0
|
||||||
self._source_list = {'TV': 'SITV', 'Tuner': 'SITUNER',
|
# Initial value 60dB, changed if we get a MVMAX
|
||||||
'Internet Radio': 'SIIRP', 'Favorites': 'SIFVP'}
|
self._volume_max = 60
|
||||||
|
self._source_list = NORMAL_INPUTS.copy()
|
||||||
|
self._source_list.update(MEDIA_MODES)
|
||||||
self._muted = False
|
self._muted = False
|
||||||
self._mediasource = ''
|
self._mediasource = ''
|
||||||
|
self._mediainfo = ''
|
||||||
|
|
||||||
|
self._should_setup_sources = True
|
||||||
|
|
||||||
|
def _setup_sources(self, telnet):
|
||||||
|
# NSFRN - Network name
|
||||||
|
self._name = self.telnet_request(telnet, 'NSFRN ?')[len('NSFRN '):]
|
||||||
|
|
||||||
|
# SSFUN - Configured sources with names
|
||||||
|
self._source_list = {}
|
||||||
|
for line in self.telnet_request(telnet, 'SSFUN ?', all_lines=True):
|
||||||
|
source, configured_name = line[len('SSFUN'):].split(" ", 1)
|
||||||
|
self._source_list[configured_name] = source
|
||||||
|
|
||||||
|
# SSSOD - Deleted sources
|
||||||
|
for line in self.telnet_request(telnet, 'SSSOD ?', all_lines=True):
|
||||||
|
source, status = line[len('SSSOD'):].split(" ", 1)
|
||||||
|
if status == 'DEL':
|
||||||
|
for pretty_name, name in self._source_list.items():
|
||||||
|
if source == name:
|
||||||
|
del self._source_list[pretty_name]
|
||||||
|
break
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def telnet_request(cls, telnet, command):
|
def telnet_request(cls, telnet, command, all_lines=False):
|
||||||
"""Execute `command` and return the response."""
|
"""Execute `command` and return the response."""
|
||||||
|
_LOGGER.debug('Sending: "%s"', command)
|
||||||
telnet.write(command.encode('ASCII') + b'\r')
|
telnet.write(command.encode('ASCII') + b'\r')
|
||||||
lines = []
|
lines = []
|
||||||
while True:
|
while True:
|
||||||
@ -68,12 +109,16 @@ class DenonDevice(MediaPlayerDevice):
|
|||||||
if not line:
|
if not line:
|
||||||
break
|
break
|
||||||
lines.append(line.decode('ASCII').strip())
|
lines.append(line.decode('ASCII').strip())
|
||||||
|
_LOGGER.debug('Recived: "%s"', line)
|
||||||
|
|
||||||
|
if all_lines:
|
||||||
|
return lines
|
||||||
return lines[0]
|
return lines[0]
|
||||||
|
|
||||||
def telnet_command(self, command):
|
def telnet_command(self, command):
|
||||||
"""Establish a telnet connection and sends `command`."""
|
"""Establish a telnet connection and sends `command`."""
|
||||||
telnet = telnetlib.Telnet(self._host)
|
telnet = telnetlib.Telnet(self._host)
|
||||||
|
_LOGGER.debug('Sending: "%s"', command)
|
||||||
telnet.write(command.encode('ASCII') + b'\r')
|
telnet.write(command.encode('ASCII') + b'\r')
|
||||||
telnet.read_very_eager() # skip response
|
telnet.read_very_eager() # skip response
|
||||||
telnet.close()
|
telnet.close()
|
||||||
@ -85,12 +130,30 @@ class DenonDevice(MediaPlayerDevice):
|
|||||||
except OSError:
|
except OSError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if self._should_setup_sources:
|
||||||
|
self._setup_sources(telnet)
|
||||||
|
self._should_setup_sources = False
|
||||||
|
|
||||||
self._pwstate = self.telnet_request(telnet, 'PW?')
|
self._pwstate = self.telnet_request(telnet, 'PW?')
|
||||||
volume_str = self.telnet_request(telnet, 'MV?')[len('MV'):]
|
for line in self.telnet_request(telnet, 'MV?', all_lines=True):
|
||||||
self._volume = int(volume_str) / 60
|
if line.startswith('MVMAX '):
|
||||||
|
# only grab two digit max, don't care about any half digit
|
||||||
|
self._volume_max = int(line[len('MVMAX '):len('MVMAX XX')])
|
||||||
|
continue
|
||||||
|
if line.startswith('MV'):
|
||||||
|
self._volume = int(line[len('MV'):])
|
||||||
self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON')
|
self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON')
|
||||||
self._mediasource = self.telnet_request(telnet, 'SI?')[len('SI'):]
|
self._mediasource = self.telnet_request(telnet, 'SI?')[len('SI'):]
|
||||||
|
|
||||||
|
if self._mediasource in MEDIA_MODES.values():
|
||||||
|
self._mediainfo = ""
|
||||||
|
answer_codes = ["NSE0", "NSE1X", "NSE2X", "NSE3X", "NSE4", "NSE5",
|
||||||
|
"NSE6", "NSE7", "NSE8"]
|
||||||
|
for line in self.telnet_request(telnet, 'NSE', all_lines=True):
|
||||||
|
self._mediainfo += line[len(answer_codes.pop()):] + '\n'
|
||||||
|
else:
|
||||||
|
self._mediainfo = self.source
|
||||||
|
|
||||||
telnet.close()
|
telnet.close()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -112,7 +175,7 @@ class DenonDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
"""Volume level of the media player (0..1)."""
|
"""Volume level of the media player (0..1)."""
|
||||||
return self._volume
|
return self._volume / self._volume_max
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_volume_muted(self):
|
def is_volume_muted(self):
|
||||||
@ -122,17 +185,27 @@ class DenonDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def source_list(self):
|
def source_list(self):
|
||||||
"""List of available input sources."""
|
"""List of available input sources."""
|
||||||
return list(self._source_list.keys())
|
return sorted(list(self._source_list.keys()))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_title(self):
|
def media_title(self):
|
||||||
"""Current media source."""
|
"""Current media info."""
|
||||||
return self._mediasource
|
return self._mediainfo
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_media_commands(self):
|
def supported_media_commands(self):
|
||||||
"""Flag of media commands that are supported."""
|
"""Flag of media commands that are supported."""
|
||||||
return SUPPORT_DENON
|
if self._mediasource in MEDIA_MODES.values():
|
||||||
|
return SUPPORT_DENON | SUPPORT_MEDIA_MODES
|
||||||
|
else:
|
||||||
|
return SUPPORT_DENON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source(self):
|
||||||
|
"""Return the current input source."""
|
||||||
|
for pretty_name, name in self._source_list.items():
|
||||||
|
if self._mediasource == name:
|
||||||
|
return pretty_name
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn off media player."""
|
"""Turn off media player."""
|
||||||
@ -148,8 +221,8 @@ class DenonDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
def set_volume_level(self, volume):
|
def set_volume_level(self, volume):
|
||||||
"""Set volume level, range 0..1."""
|
"""Set volume level, range 0..1."""
|
||||||
# 60dB max
|
self.telnet_command('MV' +
|
||||||
self.telnet_command('MV' + str(round(volume * 60)).zfill(2))
|
str(round(volume * self._volume_max)).zfill(2))
|
||||||
|
|
||||||
def mute_volume(self, mute):
|
def mute_volume(self, mute):
|
||||||
"""Mute (true) or unmute (false) media player."""
|
"""Mute (true) or unmute (false) media player."""
|
||||||
@ -163,6 +236,10 @@ class DenonDevice(MediaPlayerDevice):
|
|||||||
"""Pause media player."""
|
"""Pause media player."""
|
||||||
self.telnet_command('NS9B')
|
self.telnet_command('NS9B')
|
||||||
|
|
||||||
|
def media_stop(self):
|
||||||
|
"""Pause media player."""
|
||||||
|
self.telnet_command('NS9C')
|
||||||
|
|
||||||
def media_next_track(self):
|
def media_next_track(self):
|
||||||
"""Send the next track command."""
|
"""Send the next track command."""
|
||||||
self.telnet_command('NS9D')
|
self.telnet_command('NS9D')
|
||||||
@ -177,4 +254,4 @@ class DenonDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
def select_source(self, source):
|
def select_source(self, source):
|
||||||
"""Select input source."""
|
"""Select input source."""
|
||||||
self.telnet_command(self._source_list.get(source))
|
self.telnet_command('SI' + self._source_list.get(source))
|
||||||
|
@ -13,26 +13,27 @@ from homeassistant.components.media_player import (
|
|||||||
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||||
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
|
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
|
||||||
MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON,
|
MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON,
|
||||||
MEDIA_TYPE_MUSIC)
|
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
|
CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
|
||||||
CONF_NAME, STATE_ON)
|
CONF_NAME, STATE_ON)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['denonavr==0.1.6']
|
REQUIREMENTS = ['denonavr==0.2.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = None
|
DEFAULT_NAME = None
|
||||||
|
KEY_DENON_CACHE = 'denonavr_hosts'
|
||||||
|
|
||||||
SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \
|
SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \
|
||||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
|
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
|
||||||
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | \
|
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | \
|
||||||
SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \
|
SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \
|
||||||
SUPPORT_NEXT_TRACK
|
SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_SET
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Optional(CONF_HOST): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -41,11 +42,53 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
"""Setup the Denon platform."""
|
"""Setup the Denon platform."""
|
||||||
import denonavr
|
import denonavr
|
||||||
|
|
||||||
receiver = denonavr.DenonAVR(config.get(CONF_HOST), config.get(CONF_NAME))
|
# Initialize list with receivers to be started
|
||||||
|
receivers = []
|
||||||
|
|
||||||
add_devices([DenonDevice(receiver)])
|
cache = hass.data.get(KEY_DENON_CACHE)
|
||||||
_LOGGER.info("Denon receiver at host %s initialized",
|
if cache is None:
|
||||||
config.get(CONF_HOST))
|
cache = hass.data[KEY_DENON_CACHE] = set()
|
||||||
|
|
||||||
|
# Start assignment of host and name
|
||||||
|
# 1. option: manual setting
|
||||||
|
if config.get(CONF_HOST) is not None:
|
||||||
|
host = config.get(CONF_HOST)
|
||||||
|
name = config.get(CONF_NAME)
|
||||||
|
# Check if host not in cache, append it and save for later starting
|
||||||
|
if host not in cache:
|
||||||
|
cache.add(host)
|
||||||
|
receivers.append(
|
||||||
|
DenonDevice(denonavr.DenonAVR(host, name)))
|
||||||
|
_LOGGER.info("Denon receiver at host %s initialized", host)
|
||||||
|
# 2. option: discovery using netdisco
|
||||||
|
if discovery_info is not None:
|
||||||
|
host = discovery_info[0]
|
||||||
|
name = discovery_info[1]
|
||||||
|
# Check if host not in cache, append it and save for later starting
|
||||||
|
if host not in cache:
|
||||||
|
cache.add(host)
|
||||||
|
receivers.append(
|
||||||
|
DenonDevice(denonavr.DenonAVR(host, name)))
|
||||||
|
_LOGGER.info("Denon receiver at host %s initialized", host)
|
||||||
|
# 3. option: discovery using denonavr library
|
||||||
|
if config.get(CONF_HOST) is None and discovery_info is None:
|
||||||
|
d_receivers = denonavr.discover()
|
||||||
|
# More than one receiver could be discovered by that method
|
||||||
|
if d_receivers is not None:
|
||||||
|
for d_receiver in d_receivers:
|
||||||
|
host = d_receiver["host"]
|
||||||
|
name = d_receiver["friendlyName"]
|
||||||
|
# Check if host not in cache, append it and save for later
|
||||||
|
# starting
|
||||||
|
if host not in cache:
|
||||||
|
cache.add(host)
|
||||||
|
receivers.append(
|
||||||
|
DenonDevice(denonavr.DenonAVR(host, name)))
|
||||||
|
_LOGGER.info("Denon receiver at host %s initialized", host)
|
||||||
|
|
||||||
|
# Add all freshly discovered receivers
|
||||||
|
if receivers:
|
||||||
|
add_devices(receivers)
|
||||||
|
|
||||||
|
|
||||||
class DenonDevice(MediaPlayerDevice):
|
class DenonDevice(MediaPlayerDevice):
|
||||||
@ -107,7 +150,8 @@ class DenonDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
"""Volume level of the media player (0..1)."""
|
"""Volume level of the media player (0..1)."""
|
||||||
# Volume is send in a format like -50.0. Minimum is around -80.0
|
# Volume is sent in a format like -50.0. Minimum is -80.0,
|
||||||
|
# maximum is 18.0
|
||||||
return (float(self._volume) + 80) / 100
|
return (float(self._volume) + 80) / 100
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -240,6 +284,22 @@ class DenonDevice(MediaPlayerDevice):
|
|||||||
"""Volume down media player."""
|
"""Volume down media player."""
|
||||||
return self._receiver.volume_down()
|
return self._receiver.volume_down()
|
||||||
|
|
||||||
|
def set_volume_level(self, volume):
|
||||||
|
"""Set volume level, range 0..1."""
|
||||||
|
# Volume has to be sent in a format like -50.0. Minimum is -80.0,
|
||||||
|
# maximum is 18.0
|
||||||
|
volume_denon = float((volume * 100) - 80)
|
||||||
|
if volume_denon > 18:
|
||||||
|
volume_denon = float(18)
|
||||||
|
try:
|
||||||
|
if self._receiver.set_volume(volume_denon):
|
||||||
|
self._volume = volume_denon
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
def mute_volume(self, mute):
|
def mute_volume(self, mute):
|
||||||
"""Send mute command."""
|
"""Send mute command."""
|
||||||
return self._receiver.mute(mute)
|
return self._receiver.mute(mute)
|
||||||
|
@ -20,12 +20,15 @@ from homeassistant.const import (
|
|||||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN)
|
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN)
|
||||||
from homeassistant.helpers.event import (track_utc_time_change)
|
from homeassistant.helpers.event import (track_utc_time_change)
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
REQUIREMENTS = ['pyemby==0.1']
|
REQUIREMENTS = ['pyemby==0.2']
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||||
|
|
||||||
|
MEDIA_TYPE_TRAILER = 'trailer'
|
||||||
|
|
||||||
DEFAULT_PORT = 8096
|
DEFAULT_PORT = 8096
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -119,6 +122,8 @@ class EmbyClient(MediaPlayerDevice):
|
|||||||
self.update_sessions = update_sessions
|
self.update_sessions = update_sessions
|
||||||
self.client = client
|
self.client = client
|
||||||
self.set_device(device)
|
self.set_device(device)
|
||||||
|
self.media_status_last_position = None
|
||||||
|
self.media_status_received = None
|
||||||
|
|
||||||
def set_device(self, device):
|
def set_device(self, device):
|
||||||
"""Set the device property."""
|
"""Set the device property."""
|
||||||
@ -178,6 +183,17 @@ class EmbyClient(MediaPlayerDevice):
|
|||||||
"""Get the latest details."""
|
"""Get the latest details."""
|
||||||
self.update_devices(no_throttle=True)
|
self.update_devices(no_throttle=True)
|
||||||
self.update_sessions(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):
|
def play_percent(self):
|
||||||
"""Return current media percent complete."""
|
"""Return current media percent complete."""
|
||||||
@ -220,6 +236,8 @@ class EmbyClient(MediaPlayerDevice):
|
|||||||
return MEDIA_TYPE_TVSHOW
|
return MEDIA_TYPE_TVSHOW
|
||||||
elif media_type == 'Movie':
|
elif media_type == 'Movie':
|
||||||
return MEDIA_TYPE_VIDEO
|
return MEDIA_TYPE_VIDEO
|
||||||
|
elif media_type == 'Trailer':
|
||||||
|
return MEDIA_TYPE_TRAILER
|
||||||
return None
|
return None
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
@ -233,19 +251,32 @@ class EmbyClient(MediaPlayerDevice):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position(self):
|
||||||
|
"""Position of current playing media in seconds."""
|
||||||
|
return self.media_status_last_position
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position_updated_at(self):
|
||||||
|
"""
|
||||||
|
When was the position of the current playing media valid.
|
||||||
|
|
||||||
|
Returns value from homeassistant.util.dt.utcnow().
|
||||||
|
"""
|
||||||
|
return self.media_status_received
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_image_url(self):
|
def media_image_url(self):
|
||||||
"""Image url of current playing media."""
|
"""Image url of current playing media."""
|
||||||
if self.now_playing_item is not None:
|
if self.now_playing_item is not None:
|
||||||
try:
|
try:
|
||||||
return self.client.get_image(
|
return self.client.get_image(
|
||||||
self.now_playing_item['ThumbItemId'], 'Thumb',
|
self.now_playing_item['ThumbItemId'], 'Thumb', 0)
|
||||||
self.play_percent())
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
try:
|
try:
|
||||||
return self.client.get_image(
|
return self.client.get_image(
|
||||||
self.now_playing_item['PrimaryImageItemId'], 'Primary',
|
self.now_playing_item[
|
||||||
self.play_percent())
|
'PrimaryImageItemId'], 'Primary', 0)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -76,11 +76,17 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
import jsonrpc_requests
|
import jsonrpc_requests
|
||||||
self._name = name
|
self._name = name
|
||||||
self._url = url
|
self._url = url
|
||||||
|
self._basic_auth_url = None
|
||||||
|
|
||||||
kwargs = {'timeout': 5}
|
kwargs = {'timeout': 5}
|
||||||
|
|
||||||
if auth is not None:
|
if auth is not None:
|
||||||
kwargs['auth'] = auth
|
kwargs['auth'] = auth
|
||||||
|
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
|
||||||
|
self._basic_auth_url = \
|
||||||
|
urllib.parse.urlunsplit((scheme, '{}:{}@{}'.format
|
||||||
|
(auth[0], auth[1], netloc),
|
||||||
|
path, query, fragment))
|
||||||
|
|
||||||
self._server = jsonrpc_requests.Server(
|
self._server = jsonrpc_requests.Server(
|
||||||
'{}/jsonrpc'.format(self._url), **kwargs)
|
'{}/jsonrpc'.format(self._url), **kwargs)
|
||||||
@ -195,6 +201,11 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
url_components = urllib.parse.urlparse(self._item['thumbnail'])
|
url_components = urllib.parse.urlparse(self._item['thumbnail'])
|
||||||
|
|
||||||
if url_components.scheme == 'image':
|
if url_components.scheme == 'image':
|
||||||
|
if self._basic_auth_url is not None:
|
||||||
|
return '{}/image/{}'.format(
|
||||||
|
self._basic_auth_url,
|
||||||
|
urllib.parse.quote_plus(self._item['thumbnail']))
|
||||||
|
|
||||||
return '{}/image/{}'.format(
|
return '{}/image/{}'.format(
|
||||||
self._url,
|
self._url,
|
||||||
urllib.parse.quote_plus(self._item['thumbnail']))
|
urllib.parse.quote_plus(self._item['thumbnail']))
|
||||||
|
@ -89,7 +89,7 @@ class OnkyoDevice(MediaPlayerDevice):
|
|||||||
except (ValueError, OSError, AttributeError, AssertionError):
|
except (ValueError, OSError, AttributeError, AssertionError):
|
||||||
if self._receiver.command_socket:
|
if self._receiver.command_socket:
|
||||||
self._receiver.command_socket = None
|
self._receiver.command_socket = None
|
||||||
_LOGGER.info('Reseting connection to %s.', self._name)
|
_LOGGER.info('Resetting connection to %s.', self._name)
|
||||||
else:
|
else:
|
||||||
_LOGGER.info('%s is disconnected. Attempting to reconnect.',
|
_LOGGER.info('%s is disconnected. Attempting to reconnect.',
|
||||||
self._name)
|
self._name)
|
||||||
@ -173,7 +173,7 @@ class OnkyoDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
def turn_on(self):
|
def turn_on(self):
|
||||||
"""Turn the media player on."""
|
"""Turn the media player on."""
|
||||||
self._receiver.power_on()
|
self.command('system-power on')
|
||||||
|
|
||||||
def select_source(self, source):
|
def select_source(self, source):
|
||||||
"""Set the input source."""
|
"""Set the input source."""
|
||||||
|
@ -10,16 +10,20 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
||||||
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
||||||
|
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||||
SUPPORT_VOLUME_STEP, MediaPlayerDevice, PLATFORM_SCHEMA)
|
SUPPORT_VOLUME_STEP, MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT)
|
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['panasonic_viera==0.2']
|
REQUIREMENTS = ['panasonic_viera==0.2',
|
||||||
|
'wakeonlan==0.2.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_MAC = 'mac'
|
||||||
|
|
||||||
DEFAULT_NAME = 'Panasonic Viera TV'
|
DEFAULT_NAME = 'Panasonic Viera TV'
|
||||||
DEFAULT_PORT = 55000
|
DEFAULT_PORT = 55000
|
||||||
|
|
||||||
@ -30,6 +34,7 @@ SUPPORT_VIERATV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
|||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Optional(CONF_MAC): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
})
|
})
|
||||||
@ -40,6 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
"""Setup the Panasonic Viera TV platform."""
|
"""Setup the Panasonic Viera TV platform."""
|
||||||
from panasonic_viera import RemoteControl
|
from panasonic_viera import RemoteControl
|
||||||
|
|
||||||
|
mac = config.get(CONF_MAC)
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
port = config.get(CONF_PORT)
|
port = config.get(CONF_PORT)
|
||||||
|
|
||||||
@ -51,29 +57,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
host = vals[0]
|
host = vals[0]
|
||||||
remote = RemoteControl(host, port)
|
remote = RemoteControl(host, port)
|
||||||
add_devices([PanasonicVieraTVDevice(name, remote)])
|
add_devices([PanasonicVieraTVDevice(mac, name, remote)])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
host = config.get(CONF_HOST)
|
host = config.get(CONF_HOST)
|
||||||
remote = RemoteControl(host, port)
|
remote = RemoteControl(host, port)
|
||||||
|
|
||||||
try:
|
add_devices([PanasonicVieraTVDevice(mac, name, remote)])
|
||||||
remote.get_mute()
|
|
||||||
except OSError as error:
|
|
||||||
_LOGGER.error('Panasonic Viera TV is not available at %s:%d: %s',
|
|
||||||
host, port, error)
|
|
||||||
return False
|
|
||||||
|
|
||||||
add_devices([PanasonicVieraTVDevice(name, remote)])
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class PanasonicVieraTVDevice(MediaPlayerDevice):
|
class PanasonicVieraTVDevice(MediaPlayerDevice):
|
||||||
"""Representation of a Panasonic Viera TV."""
|
"""Representation of a Panasonic Viera TV."""
|
||||||
|
|
||||||
def __init__(self, name, remote):
|
def __init__(self, mac, name, remote):
|
||||||
"""Initialize the Panasonic device."""
|
"""Initialize the Panasonic device."""
|
||||||
|
from wakeonlan import wol
|
||||||
# Save a reference to the imported class
|
# Save a reference to the imported class
|
||||||
|
self._wol = wol
|
||||||
|
self._mac = mac
|
||||||
self._name = name
|
self._name = name
|
||||||
self._muted = False
|
self._muted = False
|
||||||
self._playing = True
|
self._playing = True
|
||||||
@ -123,8 +125,15 @@ class PanasonicVieraTVDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def supported_media_commands(self):
|
def supported_media_commands(self):
|
||||||
"""Flag of media commands that are supported."""
|
"""Flag of media commands that are supported."""
|
||||||
|
if self._mac:
|
||||||
|
return SUPPORT_VIERATV | SUPPORT_TURN_ON
|
||||||
return SUPPORT_VIERATV
|
return SUPPORT_VIERATV
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
"""Turn on the media player."""
|
||||||
|
if self._mac:
|
||||||
|
self._wol.send_magic_packet(self._mac)
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn off media player."""
|
"""Turn off media player."""
|
||||||
self.send_key('NRC_POWER-ONOFF')
|
self.send_key('NRC_POWER-ONOFF')
|
||||||
|
@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/media_player.samsungtv/
|
https://home-assistant.io/components/media_player.samsungtv/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import socket
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -26,6 +27,8 @@ DEFAULT_NAME = 'Samsung TV Remote'
|
|||||||
DEFAULT_PORT = 55000
|
DEFAULT_PORT = 55000
|
||||||
DEFAULT_TIMEOUT = 0
|
DEFAULT_TIMEOUT = 0
|
||||||
|
|
||||||
|
KNOWN_DEVICES_KEY = 'samsungtv_known_devices'
|
||||||
|
|
||||||
SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
SUPPORT_SAMSUNGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
||||||
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
||||||
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF
|
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF
|
||||||
@ -41,25 +44,42 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Setup the Samsung TV platform."""
|
"""Setup the Samsung TV platform."""
|
||||||
name = config.get(CONF_NAME)
|
known_devices = hass.data.get(KNOWN_DEVICES_KEY)
|
||||||
|
if known_devices is None:
|
||||||
|
known_devices = set()
|
||||||
|
hass.data[KNOWN_DEVICES_KEY] = known_devices
|
||||||
|
|
||||||
# Generate a configuration for the Samsung library
|
# Is this a manual configuration?
|
||||||
remote_config = {
|
if config.get(CONF_HOST) is not None:
|
||||||
'name': 'HomeAssistant',
|
host = config.get(CONF_HOST)
|
||||||
'description': config.get(CONF_NAME),
|
port = config.get(CONF_PORT)
|
||||||
'id': 'ha.component.samsung',
|
name = config.get(CONF_NAME)
|
||||||
'port': config.get(CONF_PORT),
|
timeout = config.get(CONF_TIMEOUT)
|
||||||
'host': config.get(CONF_HOST),
|
elif discovery_info is not None:
|
||||||
'timeout': config.get(CONF_TIMEOUT),
|
tv_name, model, host = discovery_info
|
||||||
}
|
name = "{} ({})".format(tv_name, model)
|
||||||
|
port = DEFAULT_PORT
|
||||||
|
timeout = DEFAULT_TIMEOUT
|
||||||
|
else:
|
||||||
|
_LOGGER.warning(
|
||||||
|
'Internal error on samsungtv component. Cannot determine device')
|
||||||
|
return
|
||||||
|
|
||||||
add_devices([SamsungTVDevice(name, remote_config)])
|
# Only add a device once, so discovered devices do not override manual
|
||||||
|
# config.
|
||||||
|
ip_addr = socket.gethostbyname(host)
|
||||||
|
if ip_addr not in known_devices:
|
||||||
|
known_devices.add(ip_addr)
|
||||||
|
add_devices([SamsungTVDevice(host, port, name, timeout)])
|
||||||
|
_LOGGER.info("Samsung TV %s:%d added as '%s'", host, port, name)
|
||||||
|
else:
|
||||||
|
_LOGGER.info("Ignoring duplicate Samsung TV %s:%d", host, port)
|
||||||
|
|
||||||
|
|
||||||
class SamsungTVDevice(MediaPlayerDevice):
|
class SamsungTVDevice(MediaPlayerDevice):
|
||||||
"""Representation of a Samsung TV."""
|
"""Representation of a Samsung TV."""
|
||||||
|
|
||||||
def __init__(self, name, config):
|
def __init__(self, host, port, name, timeout):
|
||||||
"""Initialize the Samsung device."""
|
"""Initialize the Samsung device."""
|
||||||
from samsungctl import Remote
|
from samsungctl import Remote
|
||||||
# Save a reference to the imported class
|
# Save a reference to the imported class
|
||||||
@ -71,7 +91,15 @@ class SamsungTVDevice(MediaPlayerDevice):
|
|||||||
self._playing = True
|
self._playing = True
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
self._remote = None
|
self._remote = None
|
||||||
self._config = config
|
# Generate a configuration for the Samsung library
|
||||||
|
self._config = {
|
||||||
|
'name': 'HomeAssistant',
|
||||||
|
'description': name,
|
||||||
|
'id': 'ha.component.samsung',
|
||||||
|
'port': port,
|
||||||
|
'host': host,
|
||||||
|
'timeout': timeout,
|
||||||
|
}
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Retrieve the latest data."""
|
"""Retrieve the latest data."""
|
||||||
|
@ -15,7 +15,7 @@ from homeassistant.components.media_player import (
|
|||||||
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||||
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
|
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
|
||||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA)
|
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_STOP)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID,
|
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID,
|
||||||
CONF_HOSTS)
|
CONF_HOSTS)
|
||||||
@ -36,9 +36,10 @@ _SOCO_LOGGER.setLevel(logging.ERROR)
|
|||||||
_REQUESTS_LOGGER = logging.getLogger('requests')
|
_REQUESTS_LOGGER = logging.getLogger('requests')
|
||||||
_REQUESTS_LOGGER.setLevel(logging.ERROR)
|
_REQUESTS_LOGGER.setLevel(logging.ERROR)
|
||||||
|
|
||||||
SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
|
SUPPORT_SONOS = SUPPORT_STOP | SUPPORT_PAUSE | SUPPORT_VOLUME_SET |\
|
||||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA |\
|
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\
|
||||||
SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST | SUPPORT_SELECT_SOURCE
|
SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST |\
|
||||||
|
SUPPORT_SELECT_SOURCE
|
||||||
|
|
||||||
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
|
SERVICE_GROUP_PLAYERS = 'sonos_group_players'
|
||||||
SERVICE_UNJOIN = 'sonos_unjoin'
|
SERVICE_UNJOIN = 'sonos_unjoin'
|
||||||
@ -289,6 +290,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
self._media_next_title = None
|
self._media_next_title = None
|
||||||
self._support_previous_track = False
|
self._support_previous_track = False
|
||||||
self._support_next_track = False
|
self._support_next_track = False
|
||||||
|
self._support_stop = False
|
||||||
self._support_pause = False
|
self._support_pause = False
|
||||||
self._current_track_uri = None
|
self._current_track_uri = None
|
||||||
self._current_track_is_radio_stream = False
|
self._current_track_is_radio_stream = False
|
||||||
@ -296,6 +298,8 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
self._last_avtransport_event = None
|
self._last_avtransport_event = None
|
||||||
self._is_playing_line_in = None
|
self._is_playing_line_in = None
|
||||||
self._is_playing_tv = None
|
self._is_playing_tv = None
|
||||||
|
self._favorite_sources = None
|
||||||
|
self._source_name = None
|
||||||
self.soco_snapshot = Snapshot(self._player)
|
self.soco_snapshot = Snapshot(self._player)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -423,6 +427,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
media_position = None
|
media_position = None
|
||||||
media_position_updated_at = None
|
media_position_updated_at = None
|
||||||
|
source_name = None
|
||||||
|
|
||||||
is_radio_stream = \
|
is_radio_stream = \
|
||||||
current_media_uri.startswith('x-sonosapi-stream:') or \
|
current_media_uri.startswith('x-sonosapi-stream:') or \
|
||||||
@ -433,6 +438,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
support_previous_track = False
|
support_previous_track = False
|
||||||
support_next_track = False
|
support_next_track = False
|
||||||
|
support_stop = False
|
||||||
support_pause = False
|
support_pause = False
|
||||||
|
|
||||||
if is_playing_tv:
|
if is_playing_tv:
|
||||||
@ -440,6 +446,8 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
else:
|
else:
|
||||||
media_artist = SUPPORT_SOURCE_LINEIN
|
media_artist = SUPPORT_SOURCE_LINEIN
|
||||||
|
|
||||||
|
source_name = media_artist
|
||||||
|
|
||||||
media_album_name = None
|
media_album_name = None
|
||||||
media_title = None
|
media_title = None
|
||||||
media_image_url = None
|
media_image_url = None
|
||||||
@ -450,8 +458,19 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
)
|
)
|
||||||
support_previous_track = False
|
support_previous_track = False
|
||||||
support_next_track = False
|
support_next_track = False
|
||||||
|
support_stop = False
|
||||||
support_pause = False
|
support_pause = False
|
||||||
|
|
||||||
|
source_name = 'Radio'
|
||||||
|
# Check if currently playing radio station is in favorites
|
||||||
|
favs = self._player.get_sonos_favorites()['favorites']
|
||||||
|
favc = [
|
||||||
|
fav for fav in favs if fav['uri'] == current_media_uri
|
||||||
|
]
|
||||||
|
if len(favc) == 1:
|
||||||
|
src = favc.pop()
|
||||||
|
source_name = src['title']
|
||||||
|
|
||||||
# for radio streams we set the radio station name as the
|
# for radio streams we set the radio station name as the
|
||||||
# title.
|
# title.
|
||||||
if media_artist and media_title:
|
if media_artist and media_title:
|
||||||
@ -506,6 +525,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
)
|
)
|
||||||
support_previous_track = True
|
support_previous_track = True
|
||||||
support_next_track = True
|
support_next_track = True
|
||||||
|
support_stop = True
|
||||||
support_pause = True
|
support_pause = True
|
||||||
|
|
||||||
position_info = self._player.avTransport.GetPositionInfo(
|
position_info = self._player.avTransport.GetPositionInfo(
|
||||||
@ -583,9 +603,11 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
self._current_track_is_radio_stream = is_radio_stream
|
self._current_track_is_radio_stream = is_radio_stream
|
||||||
self._support_previous_track = support_previous_track
|
self._support_previous_track = support_previous_track
|
||||||
self._support_next_track = support_next_track
|
self._support_next_track = support_next_track
|
||||||
|
self._support_stop = support_stop
|
||||||
self._support_pause = support_pause
|
self._support_pause = support_pause
|
||||||
self._is_playing_tv = is_playing_tv
|
self._is_playing_tv = is_playing_tv
|
||||||
self._is_playing_line_in = is_playing_line_in
|
self._is_playing_line_in = is_playing_line_in
|
||||||
|
self._source_name = source_name
|
||||||
|
|
||||||
# update state of the whole group
|
# update state of the whole group
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
@ -595,6 +617,8 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
if self._queue is None and self.entity_id is not None:
|
if self._queue is None and self.entity_id is not None:
|
||||||
self._subscribe_to_player_events()
|
self._subscribe_to_player_events()
|
||||||
|
favs = self._player.get_sonos_favorites().get('favorites', [])
|
||||||
|
self._favorite_sources = [fav['title'] for fav in favs]
|
||||||
else:
|
else:
|
||||||
self._player_volume = None
|
self._player_volume = None
|
||||||
self._player_volume_muted = None
|
self._player_volume_muted = None
|
||||||
@ -614,9 +638,12 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
self._current_track_is_radio_stream = False
|
self._current_track_is_radio_stream = False
|
||||||
self._support_previous_track = False
|
self._support_previous_track = False
|
||||||
self._support_next_track = False
|
self._support_next_track = False
|
||||||
|
self._support_stop = False
|
||||||
self._support_pause = False
|
self._support_pause = False
|
||||||
self._is_playing_tv = False
|
self._is_playing_tv = False
|
||||||
self._is_playing_line_in = False
|
self._is_playing_line_in = False
|
||||||
|
self._favorite_sources = None
|
||||||
|
self._source_name = None
|
||||||
|
|
||||||
self._last_avtransport_event = None
|
self._last_avtransport_event = None
|
||||||
|
|
||||||
@ -764,16 +791,15 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
supported = SUPPORT_SONOS
|
supported = SUPPORT_SONOS
|
||||||
|
|
||||||
if not self.source_list:
|
|
||||||
# some devices do not allow source selection
|
|
||||||
supported = supported ^ SUPPORT_SELECT_SOURCE
|
|
||||||
|
|
||||||
if not self._support_previous_track:
|
if not self._support_previous_track:
|
||||||
supported = supported ^ SUPPORT_PREVIOUS_TRACK
|
supported = supported ^ SUPPORT_PREVIOUS_TRACK
|
||||||
|
|
||||||
if not self._support_next_track:
|
if not self._support_next_track:
|
||||||
supported = supported ^ SUPPORT_NEXT_TRACK
|
supported = supported ^ SUPPORT_NEXT_TRACK
|
||||||
|
|
||||||
|
if not self._support_stop:
|
||||||
|
supported = supported ^ SUPPORT_STOP
|
||||||
|
|
||||||
if not self._support_pause:
|
if not self._support_pause:
|
||||||
supported = supported ^ SUPPORT_PAUSE
|
supported = supported ^ SUPPORT_PAUSE
|
||||||
|
|
||||||
@ -798,19 +824,31 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
def select_source(self, source):
|
def select_source(self, source):
|
||||||
"""Select input source."""
|
"""Select input source."""
|
||||||
if source == SUPPORT_SOURCE_LINEIN:
|
if source == SUPPORT_SOURCE_LINEIN:
|
||||||
|
self._source_name = SUPPORT_SOURCE_LINEIN
|
||||||
self._player.switch_to_line_in()
|
self._player.switch_to_line_in()
|
||||||
elif source == SUPPORT_SOURCE_TV:
|
elif source == SUPPORT_SOURCE_TV:
|
||||||
|
self._source_name = SUPPORT_SOURCE_TV
|
||||||
self._player.switch_to_tv()
|
self._player.switch_to_tv()
|
||||||
|
else:
|
||||||
|
favorites = self._player.get_sonos_favorites()['favorites']
|
||||||
|
fav = [fav for fav in favorites if fav['title'] == source]
|
||||||
|
if len(fav) == 1:
|
||||||
|
src = fav.pop()
|
||||||
|
self._source_name = src['title']
|
||||||
|
self._player.play_uri(src['uri'], src['meta'], src['title'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_list(self):
|
def source_list(self):
|
||||||
"""List of available input sources."""
|
"""List of available input sources."""
|
||||||
model_name = self._speaker_info['model_name']
|
model_name = self._speaker_info['model_name']
|
||||||
|
|
||||||
|
sources = self._favorite_sources
|
||||||
|
|
||||||
if 'PLAY:5' in model_name:
|
if 'PLAY:5' in model_name:
|
||||||
return [SUPPORT_SOURCE_LINEIN]
|
sources += [SUPPORT_SOURCE_LINEIN]
|
||||||
elif 'PLAYBAR' in model_name:
|
elif 'PLAYBAR' in model_name:
|
||||||
return [SUPPORT_SOURCE_LINEIN, SUPPORT_SOURCE_TV]
|
sources += [SUPPORT_SOURCE_LINEIN, SUPPORT_SOURCE_TV]
|
||||||
|
return sources
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source(self):
|
def source(self):
|
||||||
@ -818,12 +856,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
if self._coordinator:
|
if self._coordinator:
|
||||||
return self._coordinator.source
|
return self._coordinator.source
|
||||||
else:
|
else:
|
||||||
if self._is_playing_line_in:
|
return self._source_name
|
||||||
return SUPPORT_SOURCE_LINEIN
|
|
||||||
elif self._is_playing_tv:
|
|
||||||
return SUPPORT_SOURCE_TV
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn off media player."""
|
"""Turn off media player."""
|
||||||
@ -836,6 +869,13 @@ class SonosDevice(MediaPlayerDevice):
|
|||||||
else:
|
else:
|
||||||
self._player.play()
|
self._player.play()
|
||||||
|
|
||||||
|
def media_stop(self):
|
||||||
|
"""Send stop command."""
|
||||||
|
if self._coordinator:
|
||||||
|
self._coordinator.media_stop()
|
||||||
|
else:
|
||||||
|
self._player.stop()
|
||||||
|
|
||||||
def media_pause(self):
|
def media_pause(self):
|
||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
if self._coordinator:
|
if self._coordinator:
|
||||||
|
@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
|
|||||||
ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, ATTR_MEDIA_EPISODE,
|
ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, ATTR_MEDIA_EPISODE,
|
||||||
ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION,
|
ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION,
|
||||||
ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK,
|
ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK,
|
||||||
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED,
|
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_INPUT_SOURCE_LIST,
|
||||||
ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA,
|
ATTR_SUPPORTED_MEDIA_COMMANDS, DOMAIN, SERVICE_PLAY_MEDIA,
|
||||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||||
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST,
|
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST,
|
||||||
@ -38,6 +38,7 @@ CONF_COMMANDS = 'commands'
|
|||||||
CONF_PLATFORM = 'platform'
|
CONF_PLATFORM = 'platform'
|
||||||
CONF_SERVICE = 'service'
|
CONF_SERVICE = 'service'
|
||||||
CONF_SERVICE_DATA = 'service_data'
|
CONF_SERVICE_DATA = 'service_data'
|
||||||
|
ATTR_DATA = 'data'
|
||||||
CONF_STATE = 'state'
|
CONF_STATE = 'state'
|
||||||
|
|
||||||
OFF_STATES = [STATE_IDLE, STATE_OFF]
|
OFF_STATES = [STATE_IDLE, STATE_OFF]
|
||||||
@ -178,14 +179,15 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
|||||||
def _call_service(self, service_name, service_data=None,
|
def _call_service(self, service_name, service_data=None,
|
||||||
allow_override=False):
|
allow_override=False):
|
||||||
"""Call either a specified or active child's service."""
|
"""Call either a specified or active child's service."""
|
||||||
if allow_override and service_name in self._cmds:
|
|
||||||
call_from_config(
|
|
||||||
self.hass, self._cmds[service_name], blocking=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
if service_data is None:
|
if service_data is None:
|
||||||
service_data = {}
|
service_data = {}
|
||||||
|
|
||||||
|
if allow_override and service_name in self._cmds:
|
||||||
|
call_from_config(
|
||||||
|
self.hass, self._cmds[service_name],
|
||||||
|
variables=service_data, blocking=True)
|
||||||
|
return
|
||||||
|
|
||||||
active_child = self._child_state
|
active_child = self._child_state
|
||||||
service_data[ATTR_ENTITY_ID] = active_child.entity_id
|
service_data[ATTR_ENTITY_ID] = active_child.entity_id
|
||||||
|
|
||||||
@ -233,7 +235,7 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def volume_level(self):
|
def volume_level(self):
|
||||||
"""Volume level of entity specified in attributes or active child."""
|
"""Volume level of entity specified in attributes or active child."""
|
||||||
return self._child_attr(ATTR_MEDIA_VOLUME_LEVEL)
|
return self._override_or_child_attr(ATTR_MEDIA_VOLUME_LEVEL)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_volume_muted(self):
|
def is_volume_muted(self):
|
||||||
@ -261,6 +263,17 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
|||||||
"""Image url of current playing media."""
|
"""Image url of current playing media."""
|
||||||
return self._child_attr(ATTR_ENTITY_PICTURE)
|
return self._child_attr(ATTR_ENTITY_PICTURE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_picture(self):
|
||||||
|
"""
|
||||||
|
Return image of the media playing.
|
||||||
|
|
||||||
|
The universal media player doesn't use the parent class logic, since
|
||||||
|
the url is coming from child entity pictures which have already been
|
||||||
|
sent through the API proxy.
|
||||||
|
"""
|
||||||
|
return self.media_image_url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_title(self):
|
def media_title(self):
|
||||||
"""Title of current playing media."""
|
"""Title of current playing media."""
|
||||||
@ -322,9 +335,14 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
|||||||
return self._child_attr(ATTR_APP_NAME)
|
return self._child_attr(ATTR_APP_NAME)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_source(self):
|
def source(self):
|
||||||
""""Return the current input source of the device."""
|
""""Return the current input source of the device."""
|
||||||
return self._child_attr(ATTR_INPUT_SOURCE)
|
return self._override_or_child_attr(ATTR_INPUT_SOURCE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_list(self):
|
||||||
|
"""List of available input sources."""
|
||||||
|
return self._override_or_child_attr(ATTR_INPUT_SOURCE_LIST)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_media_commands(self):
|
def supported_media_commands(self):
|
||||||
@ -340,6 +358,8 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
|||||||
SERVICE_VOLUME_DOWN]]):
|
SERVICE_VOLUME_DOWN]]):
|
||||||
flags |= SUPPORT_VOLUME_STEP
|
flags |= SUPPORT_VOLUME_STEP
|
||||||
flags &= ~SUPPORT_VOLUME_SET
|
flags &= ~SUPPORT_VOLUME_SET
|
||||||
|
elif SERVICE_VOLUME_SET in self._cmds:
|
||||||
|
flags |= SUPPORT_VOLUME_SET
|
||||||
|
|
||||||
if SERVICE_VOLUME_MUTE in self._cmds and \
|
if SERVICE_VOLUME_MUTE in self._cmds and \
|
||||||
ATTR_MEDIA_VOLUME_MUTED in self._attrs:
|
ATTR_MEDIA_VOLUME_MUTED in self._attrs:
|
||||||
@ -376,7 +396,7 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
|||||||
def set_volume_level(self, volume_level):
|
def set_volume_level(self, volume_level):
|
||||||
"""Set volume level, range 0..1."""
|
"""Set volume level, range 0..1."""
|
||||||
data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level}
|
data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level}
|
||||||
self._call_service(SERVICE_VOLUME_SET, data)
|
self._call_service(SERVICE_VOLUME_SET, data, allow_override=True)
|
||||||
|
|
||||||
def media_play(self):
|
def media_play(self):
|
||||||
"""Send play commmand."""
|
"""Send play commmand."""
|
||||||
@ -424,7 +444,7 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
|||||||
def select_source(self, source):
|
def select_source(self, source):
|
||||||
"""Set the input source."""
|
"""Set the input source."""
|
||||||
data = {ATTR_INPUT_SOURCE: source}
|
data = {ATTR_INPUT_SOURCE: source}
|
||||||
self._call_service(SERVICE_SELECT_SOURCE, data)
|
self._call_service(SERVICE_SELECT_SOURCE, data, allow_override=True)
|
||||||
|
|
||||||
def clear_playlist(self):
|
def clear_playlist(self):
|
||||||
"""Clear players playlist."""
|
"""Clear players playlist."""
|
||||||
|
152
homeassistant/components/media_player/vlc.py
Normal file
152
homeassistant/components/media_player/vlc.py
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
"""
|
||||||
|
Provide functionality to interact with vlc devices on the network.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/media_player.vlc/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.media_player import (
|
||||||
|
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||||
|
MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC)
|
||||||
|
from homeassistant.const import (CONF_NAME, STATE_IDLE, STATE_PAUSED,
|
||||||
|
STATE_PLAYING)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
REQUIREMENTS = ['python-vlc==1.1.2']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
SUPPORT_VLC = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||||
|
SUPPORT_PLAY_MEDIA
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the vlc platform."""
|
||||||
|
add_devices([VlcDevice(config.get(CONF_NAME))])
|
||||||
|
|
||||||
|
|
||||||
|
class VlcDevice(MediaPlayerDevice):
|
||||||
|
"""Representation of a vlc player."""
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
"""Initialize the vlc device."""
|
||||||
|
import vlc
|
||||||
|
self._instance = vlc.Instance()
|
||||||
|
self._vlc = self._instance.media_player_new()
|
||||||
|
self._name = name
|
||||||
|
self._volume = None
|
||||||
|
self._muted = None
|
||||||
|
self._state = None
|
||||||
|
self._media_position_updated_at = None
|
||||||
|
self._media_position = None
|
||||||
|
self._media_duration = None
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest details from the device."""
|
||||||
|
import vlc
|
||||||
|
status = self._vlc.get_state()
|
||||||
|
if status == vlc.State.Playing:
|
||||||
|
self._state = STATE_PLAYING
|
||||||
|
elif status == vlc.State.Paused:
|
||||||
|
self._state = STATE_PAUSED
|
||||||
|
else:
|
||||||
|
self._state = STATE_IDLE
|
||||||
|
self._media_duration = self._vlc.get_length()/1000
|
||||||
|
self._media_position = self._vlc.get_position() * self._media_duration
|
||||||
|
self._media_position_updated_at = dt_util.utcnow()
|
||||||
|
|
||||||
|
self._volume = self._vlc.audio_get_volume() / 100
|
||||||
|
self._muted = (self._vlc.audio_get_mute() == 1)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the device."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def volume_level(self):
|
||||||
|
"""Volume level of the media player (0..1)."""
|
||||||
|
return self._volume
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_volume_muted(self):
|
||||||
|
"""Boolean if volume is currently muted."""
|
||||||
|
return self._muted
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_media_commands(self):
|
||||||
|
"""Flag of media commands that are supported."""
|
||||||
|
return SUPPORT_VLC
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_content_type(self):
|
||||||
|
"""Content type of current playing media."""
|
||||||
|
return MEDIA_TYPE_MUSIC
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_duration(self):
|
||||||
|
"""Duration of current playing media in seconds."""
|
||||||
|
return self._media_duration
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position(self):
|
||||||
|
"""Position of current playing media in seconds."""
|
||||||
|
return self._media_position
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media_position_updated_at(self):
|
||||||
|
"""When was the position of the current playing media valid."""
|
||||||
|
return self._media_position_updated_at
|
||||||
|
|
||||||
|
def media_seek(self, position):
|
||||||
|
"""Seek the media to a specific location."""
|
||||||
|
track_length = self._vlc.get_length()/1000
|
||||||
|
self._vlc.set_position(position/track_length)
|
||||||
|
|
||||||
|
def mute_volume(self, mute):
|
||||||
|
"""Mute the volume."""
|
||||||
|
self._vlc.audio_set_mute(mute)
|
||||||
|
self._muted = mute
|
||||||
|
|
||||||
|
def set_volume_level(self, volume):
|
||||||
|
"""Set volume level, range 0..1."""
|
||||||
|
self._vlc.audio_set_volume(int(volume * 100))
|
||||||
|
self._volume = volume
|
||||||
|
|
||||||
|
def media_play(self):
|
||||||
|
"""Send play commmand."""
|
||||||
|
self._vlc.play()
|
||||||
|
self._state = STATE_PLAYING
|
||||||
|
|
||||||
|
def media_pause(self):
|
||||||
|
"""Send pause command."""
|
||||||
|
self._vlc.pause()
|
||||||
|
self._state = STATE_PAUSED
|
||||||
|
|
||||||
|
def play_media(self, media_type, media_id, **kwargs):
|
||||||
|
"""Play media from a URL or file."""
|
||||||
|
if not media_type == MEDIA_TYPE_MUSIC:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Invalid media type %s. Only %s is supported",
|
||||||
|
media_type, MEDIA_TYPE_MUSIC)
|
||||||
|
return
|
||||||
|
self._vlc.set_media(self._instance.media_new(media_id))
|
||||||
|
self._vlc.play()
|
||||||
|
self._state = STATE_PLAYING
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Support for interface with an LG WebOS TV.
|
Support for interface with an LG webOS Smart TV.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/media_player.webostv/
|
https://home-assistant.io/components/media_player.webostv/
|
||||||
@ -12,53 +12,48 @@ import voluptuous as vol
|
|||||||
|
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
|
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
|
||||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
||||||
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||||
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
|
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
|
||||||
MediaPlayerDevice, PLATFORM_SCHEMA)
|
MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_CUSTOMIZE, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
|
CONF_HOST, CONF_MAC, CONF_CUSTOMIZE, STATE_OFF,
|
||||||
|
STATE_PLAYING, STATE_PAUSED,
|
||||||
STATE_UNKNOWN, CONF_NAME)
|
STATE_UNKNOWN, CONF_NAME)
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv'
|
REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv'
|
||||||
'/archive/v0.1.2.zip'
|
'/archive/v0.1.2.zip'
|
||||||
'#pylgtv==0.1.2']
|
'#pylgtv==0.1.2',
|
||||||
|
'websockets==3.2',
|
||||||
|
'wakeonlan==0.2.2']
|
||||||
|
|
||||||
_CONFIGURING = {}
|
_CONFIGURING = {} # type: Dict[str, str]
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_SOURCES = 'sources'
|
CONF_SOURCES = 'sources'
|
||||||
|
|
||||||
DEFAULT_NAME = 'LG WebOS Smart TV'
|
DEFAULT_NAME = 'LG webOS Smart TV'
|
||||||
|
|
||||||
SUPPORT_WEBOSTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
|
SUPPORT_WEBOSTV = SUPPORT_TURN_OFF | \
|
||||||
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \
|
||||||
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \
|
SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \
|
||||||
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA
|
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||||
|
|
||||||
WEBOS_APP_LIVETV = 'com.webos.app.livetv'
|
|
||||||
WEBOS_APP_YOUTUBE = 'youtube.leanback.v4'
|
|
||||||
WEBOS_APP_MAKO = 'makotv'
|
|
||||||
|
|
||||||
WEBOS_APPS_SHORT = {
|
|
||||||
'livetv': WEBOS_APP_LIVETV,
|
|
||||||
'youtube': WEBOS_APP_YOUTUBE,
|
|
||||||
'makotv': WEBOS_APP_MAKO
|
|
||||||
}
|
|
||||||
|
|
||||||
CUSTOMIZE_SCHEMA = vol.Schema({
|
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||||
vol.Optional(CONF_SOURCES):
|
vol.Optional(CONF_SOURCES):
|
||||||
vol.All(cv.ensure_list, [vol.In(WEBOS_APPS_SHORT)]),
|
vol.All(cv.ensure_list, [cv.string]),
|
||||||
})
|
})
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_HOST): cv.string,
|
vol.Optional(CONF_HOST): cv.string,
|
||||||
|
vol.Optional(CONF_MAC): cv.string,
|
||||||
vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA,
|
vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -79,15 +74,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if host in _CONFIGURING:
|
if host in _CONFIGURING:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
mac = config.get(CONF_MAC)
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
customize = config.get(CONF_CUSTOMIZE)
|
customize = config.get(CONF_CUSTOMIZE)
|
||||||
setup_tv(host, name, customize, hass, add_devices)
|
setup_tv(host, mac, name, customize, hass, add_devices)
|
||||||
|
|
||||||
|
|
||||||
def setup_tv(host, name, customize, hass, add_devices):
|
def setup_tv(host, mac, name, customize, hass, add_devices):
|
||||||
"""Setup a phue bridge based on host parameter."""
|
"""Setup a LG WebOS TV based on host parameter."""
|
||||||
from pylgtv import WebOsClient
|
from pylgtv import WebOsClient
|
||||||
from pylgtv import PyLGTVPairException
|
from pylgtv import PyLGTVPairException
|
||||||
|
from websockets.exceptions import ConnectionClosed
|
||||||
|
|
||||||
client = WebOsClient(host)
|
client = WebOsClient(host)
|
||||||
|
|
||||||
@ -98,15 +95,16 @@ def setup_tv(host, name, customize, hass, add_devices):
|
|||||||
client.register()
|
client.register()
|
||||||
except PyLGTVPairException:
|
except PyLGTVPairException:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Connected to LG WebOS TV %s but not paired", host)
|
"Connected to LG webOS TV %s but not paired", host)
|
||||||
return
|
return
|
||||||
except OSError:
|
except (OSError, ConnectionClosed):
|
||||||
_LOGGER.error("Unable to connect to host %s", host)
|
_LOGGER.error("Unable to connect to host %s", host)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
# Not registered, request configuration.
|
# Not registered, request configuration.
|
||||||
_LOGGER.warning("LG WebOS TV %s needs to be paired", host)
|
_LOGGER.warning("LG webOS TV %s needs to be paired", host)
|
||||||
request_configuration(host, name, customize, hass, add_devices)
|
request_configuration(
|
||||||
|
host, mac, name, customize, hass, add_devices)
|
||||||
return
|
return
|
||||||
|
|
||||||
# If we came here and configuring this host, mark as done.
|
# If we came here and configuring this host, mark as done.
|
||||||
@ -115,10 +113,11 @@ def setup_tv(host, name, customize, hass, add_devices):
|
|||||||
configurator = get_component('configurator')
|
configurator = get_component('configurator')
|
||||||
configurator.request_done(request_id)
|
configurator.request_done(request_id)
|
||||||
|
|
||||||
add_devices([LgWebOSDevice(host, name, customize)])
|
add_devices([LgWebOSDevice(host, mac, name, customize)], True)
|
||||||
|
|
||||||
|
|
||||||
def request_configuration(host, name, customize, hass, add_devices):
|
def request_configuration(
|
||||||
|
host, mac, name, customize, hass, add_devices):
|
||||||
"""Request configuration steps from the user."""
|
"""Request configuration steps from the user."""
|
||||||
configurator = get_component('configurator')
|
configurator = get_component('configurator')
|
||||||
|
|
||||||
@ -131,10 +130,10 @@ def request_configuration(host, name, customize, hass, add_devices):
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def lgtv_configuration_callback(data):
|
def lgtv_configuration_callback(data):
|
||||||
"""The actions to do when our configuration callback is called."""
|
"""The actions to do when our configuration callback is called."""
|
||||||
setup_tv(host, name, customize, hass, add_devices)
|
setup_tv(host, mac, name, customize, hass, add_devices)
|
||||||
|
|
||||||
_CONFIGURING[host] = configurator.request_config(
|
_CONFIGURING[host] = configurator.request_config(
|
||||||
hass, 'LG WebOS TV', lgtv_configuration_callback,
|
hass, name, lgtv_configuration_callback,
|
||||||
description='Click start and accept the pairing request on your TV.',
|
description='Click start and accept the pairing request on your TV.',
|
||||||
description_image='/static/images/config_webos.png',
|
description_image='/static/images/config_webos.png',
|
||||||
submit_caption='Start pairing request'
|
submit_caption='Start pairing request'
|
||||||
@ -144,10 +143,13 @@ def request_configuration(host, name, customize, hass, add_devices):
|
|||||||
class LgWebOSDevice(MediaPlayerDevice):
|
class LgWebOSDevice(MediaPlayerDevice):
|
||||||
"""Representation of a LG WebOS TV."""
|
"""Representation of a LG WebOS TV."""
|
||||||
|
|
||||||
def __init__(self, host, name, customize):
|
def __init__(self, host, mac, name, customize):
|
||||||
"""Initialize the webos device."""
|
"""Initialize the webos device."""
|
||||||
from pylgtv import WebOsClient
|
from pylgtv import WebOsClient
|
||||||
|
from wakeonlan import wol
|
||||||
self._client = WebOsClient(host)
|
self._client = WebOsClient(host)
|
||||||
|
self._wol = wol
|
||||||
|
self._mac = mac
|
||||||
self._customize = customize
|
self._customize = customize
|
||||||
|
|
||||||
self._name = name
|
self._name = name
|
||||||
@ -158,15 +160,14 @@ class LgWebOSDevice(MediaPlayerDevice):
|
|||||||
self._volume = 0
|
self._volume = 0
|
||||||
self._current_source = None
|
self._current_source = None
|
||||||
self._current_source_id = None
|
self._current_source_id = None
|
||||||
self._source_list = None
|
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
self._app_list = None
|
self._source_list = {}
|
||||||
|
self._app_list = {}
|
||||||
self.update()
|
|
||||||
|
|
||||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Retrieve the latest data."""
|
"""Retrieve the latest data."""
|
||||||
|
from websockets.exceptions import ConnectionClosed
|
||||||
try:
|
try:
|
||||||
self._state = STATE_PLAYING
|
self._state = STATE_PLAYING
|
||||||
self._muted = self._client.get_muted()
|
self._muted = self._client.get_muted()
|
||||||
@ -175,20 +176,16 @@ class LgWebOSDevice(MediaPlayerDevice):
|
|||||||
self._source_list = {}
|
self._source_list = {}
|
||||||
self._app_list = {}
|
self._app_list = {}
|
||||||
|
|
||||||
custom_sources = []
|
custom_sources = self._customize.get(CONF_SOURCES, [])
|
||||||
for source in self._customize.get(CONF_SOURCES, []):
|
|
||||||
app_id = WEBOS_APPS_SHORT.get(source, None)
|
|
||||||
if app_id:
|
|
||||||
custom_sources.append(app_id)
|
|
||||||
else:
|
|
||||||
custom_sources.append(source)
|
|
||||||
|
|
||||||
for app in self._client.get_apps():
|
for app in self._client.get_apps():
|
||||||
self._app_list[app['id']] = app
|
self._app_list[app['id']] = app
|
||||||
if app['id'] == self._current_source_id:
|
if app['id'] == self._current_source_id:
|
||||||
self._current_source = app['title']
|
self._current_source = app['title']
|
||||||
self._source_list[app['title']] = app
|
self._source_list[app['title']] = app
|
||||||
if app['id'] in custom_sources:
|
elif (app['id'] in custom_sources or
|
||||||
|
any(word in app['title'] for word in custom_sources) or
|
||||||
|
any(word in app['id'] for word in custom_sources)):
|
||||||
self._source_list[app['title']] = app
|
self._source_list[app['title']] = app
|
||||||
|
|
||||||
for source in self._client.get_inputs():
|
for source in self._client.get_inputs():
|
||||||
@ -197,7 +194,7 @@ class LgWebOSDevice(MediaPlayerDevice):
|
|||||||
app = self._app_list[source['appId']]
|
app = self._app_list[source['appId']]
|
||||||
self._source_list[app['title']] = app
|
self._source_list[app['title']] = app
|
||||||
|
|
||||||
except OSError:
|
except (OSError, ConnectionClosed):
|
||||||
self._state = STATE_OFF
|
self._state = STATE_OFF
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -245,12 +242,23 @@ class LgWebOSDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def supported_media_commands(self):
|
def supported_media_commands(self):
|
||||||
"""Flag of media commands that are supported."""
|
"""Flag of media commands that are supported."""
|
||||||
|
if self._mac:
|
||||||
|
return SUPPORT_WEBOSTV | SUPPORT_TURN_ON
|
||||||
return SUPPORT_WEBOSTV
|
return SUPPORT_WEBOSTV
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
"""Turn off media player."""
|
"""Turn off media player."""
|
||||||
|
from websockets.exceptions import ConnectionClosed
|
||||||
self._state = STATE_OFF
|
self._state = STATE_OFF
|
||||||
self._client.power_off()
|
try:
|
||||||
|
self._client.power_off()
|
||||||
|
except (OSError, ConnectionClosed):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
"""Turn on the media player."""
|
||||||
|
if self._mac:
|
||||||
|
self._wol.send_magic_packet(self._mac)
|
||||||
|
|
||||||
def volume_up(self):
|
def volume_up(self):
|
||||||
"""Volume up the media player."""
|
"""Volume up the media player."""
|
||||||
|
@ -18,7 +18,7 @@ from homeassistant.util import Throttle
|
|||||||
|
|
||||||
REQUIREMENTS = [
|
REQUIREMENTS = [
|
||||||
'https://github.com/jabesq/netatmo-api-python/archive/'
|
'https://github.com/jabesq/netatmo-api-python/archive/'
|
||||||
'v0.7.0.zip#lnetatmo==0.7.0']
|
'v0.8.0.zip#lnetatmo==0.8.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ NETATMO_AUTH = None
|
|||||||
DEFAULT_DISCOVERY = True
|
DEFAULT_DISCOVERY = True
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
||||||
|
MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=10)
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
@ -72,10 +73,11 @@ class WelcomeData(object):
|
|||||||
self.auth = auth
|
self.auth = auth
|
||||||
self.welcomedata = None
|
self.welcomedata = None
|
||||||
self.camera_names = []
|
self.camera_names = []
|
||||||
|
self.module_names = []
|
||||||
self.home = home
|
self.home = home
|
||||||
|
|
||||||
def get_camera_names(self):
|
def get_camera_names(self):
|
||||||
"""Return all module available on the API as a list."""
|
"""Return all camera available on the API as a list."""
|
||||||
self.camera_names = []
|
self.camera_names = []
|
||||||
self.update()
|
self.update()
|
||||||
if not self.home:
|
if not self.home:
|
||||||
@ -87,8 +89,24 @@ class WelcomeData(object):
|
|||||||
self.camera_names.append(camera['name'])
|
self.camera_names.append(camera['name'])
|
||||||
return self.camera_names
|
return self.camera_names
|
||||||
|
|
||||||
|
def get_module_names(self, camera_name):
|
||||||
|
"""Return all module available on the API as a list."""
|
||||||
|
self.module_names = []
|
||||||
|
self.update()
|
||||||
|
cam_id = self.welcomedata.cameraByName(camera=camera_name,
|
||||||
|
home=self.home)['id']
|
||||||
|
for module in self.welcomedata.modules.values():
|
||||||
|
if cam_id == module['cam_id']:
|
||||||
|
self.module_names.append(module['name'])
|
||||||
|
return self.module_names
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Call the Netatmo API to update the data."""
|
"""Call the Netatmo API to update the data."""
|
||||||
import lnetatmo
|
import lnetatmo
|
||||||
self.welcomedata = lnetatmo.WelcomeData(self.auth)
|
self.welcomedata = lnetatmo.WelcomeData(self.auth, size=100)
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES)
|
||||||
|
def update_event(self):
|
||||||
|
"""Call the Netatmo API to update the list of events."""
|
||||||
|
self.welcomedata.updateEvent(home=self.home)
|
||||||
|
@ -77,7 +77,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.In(TRANSPARENCIES.keys()),
|
vol.In(TRANSPARENCIES.keys()),
|
||||||
vol.Optional(CONF_COLOR, default=DEFAULT_COLOR):
|
vol.Optional(CONF_COLOR, default=DEFAULT_COLOR):
|
||||||
vol.In(COLORS.keys()),
|
vol.In(COLORS.keys()),
|
||||||
vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): cv.string,
|
|
||||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
|
||||||
vol.Optional(CONF_INTERRUPT, default=DEFAULT_INTERRUPT): cv.boolean,
|
vol.Optional(CONF_INTERRUPT, default=DEFAULT_INTERRUPT): cv.boolean,
|
||||||
})
|
})
|
||||||
|
@ -9,6 +9,7 @@ import smtplib
|
|||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.image import MIMEImage
|
from email.mime.image import MIMEImage
|
||||||
|
import email.utils
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -18,6 +19,7 @@ from homeassistant.components.notify import (
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SENDER, CONF_RECIPIENT)
|
CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SENDER, CONF_RECIPIENT)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -134,6 +136,8 @@ class MailNotificationService(BaseNotificationService):
|
|||||||
msg['To'] = self.recipient
|
msg['To'] = self.recipient
|
||||||
msg['From'] = self._sender
|
msg['From'] = self._sender
|
||||||
msg['X-Mailer'] = 'HomeAssistant'
|
msg['X-Mailer'] = 'HomeAssistant'
|
||||||
|
msg['Date'] = email.utils.format_datetime(dt_util.now())
|
||||||
|
msg['Message-Id'] = email.utils.make_msgid()
|
||||||
|
|
||||||
return self._send_email(msg)
|
return self._send_email(msg)
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['python-telegram-bot==5.2.0']
|
REQUIREMENTS = ['python-telegram-bot==5.3.0']
|
||||||
|
|
||||||
ATTR_PHOTO = 'photo'
|
ATTR_PHOTO = 'photo'
|
||||||
ATTR_DOCUMENT = 'document'
|
ATTR_DOCUMENT = 'document'
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
"""Support for collecting data from the ARWN project.
|
"""
|
||||||
|
Support for collecting data from the ARWN project.
|
||||||
For more details about this platform, please refer to the
|
|
||||||
documentation at https://home-assistant.io/components/sensor.arwn/
|
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.arwn/
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from homeassistant.helpers.entity import Entity
|
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS)
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
DEPENDENCIES = ['mqtt']
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['mqtt']
|
||||||
|
DOMAIN = 'arwn'
|
||||||
|
|
||||||
DOMAIN = "arwn"
|
|
||||||
TOPIC = 'arwn/#'
|
|
||||||
SENSORS = {}
|
SENSORS = {}
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
TOPIC = 'arwn/#'
|
||||||
|
|
||||||
|
|
||||||
def discover_sensors(topic, payload):
|
def discover_sensors(topic, payload):
|
||||||
@ -25,23 +27,23 @@ def discover_sensors(topic, payload):
|
|||||||
parts = topic.split('/')
|
parts = topic.split('/')
|
||||||
unit = payload.get('units', '')
|
unit = payload.get('units', '')
|
||||||
domain = parts[1]
|
domain = parts[1]
|
||||||
if domain == "temperature":
|
if domain == 'temperature':
|
||||||
name = parts[2]
|
name = parts[2]
|
||||||
if unit == "F":
|
if unit == 'F':
|
||||||
unit = TEMP_FAHRENHEIT
|
unit = TEMP_FAHRENHEIT
|
||||||
else:
|
else:
|
||||||
unit = TEMP_CELSIUS
|
unit = TEMP_CELSIUS
|
||||||
return (ArwnSensor(name, 'temp', unit),)
|
return ArwnSensor(name, 'temp', unit)
|
||||||
if domain == "barometer":
|
if domain == 'barometer':
|
||||||
return (ArwnSensor("Barometer", 'pressure', unit),)
|
return ArwnSensor('Barometer', 'pressure', unit)
|
||||||
if domain == "wind":
|
if domain == 'wind':
|
||||||
return (ArwnSensor("Wind Speed", 'speed', unit),
|
return (ArwnSensor('Wind Speed', 'speed', unit),
|
||||||
ArwnSensor("Wind Gust", 'gust', unit),
|
ArwnSensor('Wind Gust', 'gust', unit),
|
||||||
ArwnSensor("Wind Direction", 'direction', '°'))
|
ArwnSensor('Wind Direction', 'direction', '°'))
|
||||||
|
|
||||||
|
|
||||||
def _slug(name):
|
def _slug(name):
|
||||||
return "sensor.arwn_%s" % slugify(name)
|
return 'sensor.arwn_{}'.format(slugify(name))
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
@ -84,7 +86,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
|
|
||||||
class ArwnSensor(Entity):
|
class ArwnSensor(Entity):
|
||||||
"""Represents an ARWN sensor."""
|
"""Representation of an ARWN sensor."""
|
||||||
|
|
||||||
def __init__(self, name, state_key, units):
|
def __init__(self, name, state_key, units):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
|
130
homeassistant/components/sensor/broadlink.py
Normal file
130
homeassistant/components/sensor/broadlink.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
|
||||||
|
"""
|
||||||
|
Support for the Broadlink RM2 Pro (only temperature) and A1 devices.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.broadlink/
|
||||||
|
"""
|
||||||
|
from datetime import timedelta
|
||||||
|
import binascii
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.const import (CONF_HOST, CONF_MAC,
|
||||||
|
CONF_MONITORED_CONDITIONS,
|
||||||
|
CONF_NAME, TEMP_CELSIUS, CONF_TIMEOUT)
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
REQUIREMENTS = ['broadlink==0.2']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_UPDATE_INTERVAL = 'update_interval'
|
||||||
|
DEVICE_DEFAULT_NAME = 'Broadlink sensor'
|
||||||
|
DEFAULT_TIMEOUT = 10
|
||||||
|
|
||||||
|
SENSOR_TYPES = {
|
||||||
|
'temperature': ['Temperature', TEMP_CELSIUS],
|
||||||
|
'air_quality': ['Air Quality', ' '],
|
||||||
|
'humidity': ['Humidity', '%'],
|
||||||
|
'light': ['Light', ' '],
|
||||||
|
'noise': ['Noise', ' ']
|
||||||
|
}
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_NAME, default=DEVICE_DEFAULT_NAME): vol.Coerce(str),
|
||||||
|
vol.Optional(CONF_MONITORED_CONDITIONS, default=[]):
|
||||||
|
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||||
|
vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=300)): (
|
||||||
|
vol.All(cv.time_period, cv.positive_timedelta)),
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Required(CONF_MAC): cv.string,
|
||||||
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the Broadlink device sensors."""
|
||||||
|
mac = config.get(CONF_MAC).encode().replace(b':', b'')
|
||||||
|
mac_addr = binascii.unhexlify(mac)
|
||||||
|
broadlink_data = BroadlinkData(
|
||||||
|
config.get(CONF_UPDATE_INTERVAL),
|
||||||
|
config.get(CONF_HOST),
|
||||||
|
mac_addr, config.get(CONF_TIMEOUT))
|
||||||
|
|
||||||
|
dev = []
|
||||||
|
for variable in config[CONF_MONITORED_CONDITIONS]:
|
||||||
|
dev.append(BroadlinkSensor(
|
||||||
|
config.get(CONF_NAME),
|
||||||
|
broadlink_data,
|
||||||
|
variable))
|
||||||
|
add_devices(dev)
|
||||||
|
|
||||||
|
|
||||||
|
class BroadlinkSensor(Entity):
|
||||||
|
"""Representation of a Broadlink device sensor."""
|
||||||
|
|
||||||
|
def __init__(self, name, broadlink_data, sensor_type):
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self._name = "%s %s" % (name, SENSOR_TYPES[sensor_type][0])
|
||||||
|
self._state = None
|
||||||
|
self._type = sensor_type
|
||||||
|
self._broadlink_data = broadlink_data
|
||||||
|
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit this state is expressed in."""
|
||||||
|
return self._unit_of_measurement
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest data from the sensor."""
|
||||||
|
self._broadlink_data.update()
|
||||||
|
if self._broadlink_data.data is None:
|
||||||
|
return
|
||||||
|
self._state = self._broadlink_data.data[self._type]
|
||||||
|
|
||||||
|
|
||||||
|
class BroadlinkData(object):
|
||||||
|
"""Representation of a Broadlink data object."""
|
||||||
|
|
||||||
|
def __init__(self, interval, ip_addr, mac_addr, timeout):
|
||||||
|
"""Initialize the data object."""
|
||||||
|
import broadlink
|
||||||
|
self.data = None
|
||||||
|
self._device = broadlink.a1((ip_addr, 80), mac_addr)
|
||||||
|
self._device.timeout = timeout
|
||||||
|
self.update = Throttle(interval)(self._update)
|
||||||
|
try:
|
||||||
|
self._device.auth()
|
||||||
|
except socket.timeout:
|
||||||
|
_LOGGER.error("Failed to connect to device.")
|
||||||
|
|
||||||
|
def _update(self, retry=2):
|
||||||
|
try:
|
||||||
|
self.data = self._device.check_sensors_raw()
|
||||||
|
except socket.timeout as error:
|
||||||
|
if retry < 1:
|
||||||
|
_LOGGER.error(error)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self._device.auth()
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
return self._update(max(0, retry-1))
|
@ -77,7 +77,7 @@ class CoinMarketCapSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self._ticker.get('price_usd')
|
return round(float(self._ticker.get('price_usd')), 2)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
|
@ -104,19 +104,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
else:
|
else:
|
||||||
units = 'us'
|
units = 'us'
|
||||||
|
|
||||||
# Create a data fetcher to support all of the configured sensors. Then make
|
forecast_data = DarkSkyData(
|
||||||
# the first call to init the data and confirm we can connect.
|
api_key=config.get(CONF_API_KEY, None),
|
||||||
try:
|
latitude=hass.config.latitude,
|
||||||
forecast_data = DarkSkyData(
|
longitude=hass.config.longitude,
|
||||||
api_key=config.get(CONF_API_KEY, None),
|
units=units,
|
||||||
latitude=hass.config.latitude,
|
interval=config.get(CONF_UPDATE_INTERVAL))
|
||||||
longitude=hass.config.longitude,
|
forecast_data.update()
|
||||||
units=units,
|
forecast_data.update_currently()
|
||||||
interval=config.get(CONF_UPDATE_INTERVAL))
|
|
||||||
forecast_data.update()
|
# If connection failed don't setup platform.
|
||||||
forecast_data.update_currently()
|
if forecast_data.data is None:
|
||||||
except ValueError as error:
|
|
||||||
_LOGGER.error(error)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
@ -227,7 +225,10 @@ class DarkSkySensor(Entity):
|
|||||||
If the sensor type is unknown, the current state is returned.
|
If the sensor type is unknown, the current state is returned.
|
||||||
"""
|
"""
|
||||||
lookup_type = convert_to_camel(self.type)
|
lookup_type = convert_to_camel(self.type)
|
||||||
state = getattr(data, lookup_type, 0)
|
state = getattr(data, lookup_type, None)
|
||||||
|
|
||||||
|
if state is None:
|
||||||
|
return state
|
||||||
|
|
||||||
# Some state data needs to be rounded to whole values or converted to
|
# Some state data needs to be rounded to whole values or converted to
|
||||||
# percentages
|
# percentages
|
||||||
@ -284,21 +285,22 @@ class DarkSkyData(object):
|
|||||||
self.data = forecastio.load_forecast(
|
self.data = forecastio.load_forecast(
|
||||||
self._api_key, self.latitude, self.longitude, units=self.units)
|
self._api_key, self.latitude, self.longitude, units=self.units)
|
||||||
except (ConnectError, HTTPError, Timeout, ValueError) as error:
|
except (ConnectError, HTTPError, Timeout, ValueError) as error:
|
||||||
raise ValueError("Unable to init Dark Sky. %s", error)
|
_LOGGER.error("Unable to connect to Dark Sky. %s", error)
|
||||||
self.unit_system = self.data.json['flags']['units']
|
self.data = None
|
||||||
|
self.unit_system = self.data and self.data.json['flags']['units']
|
||||||
|
|
||||||
def _update_currently(self):
|
def _update_currently(self):
|
||||||
"""Update currently data."""
|
"""Update currently data."""
|
||||||
self.data_currently = self.data.currently()
|
self.data_currently = self.data and self.data.currently()
|
||||||
|
|
||||||
def _update_minutely(self):
|
def _update_minutely(self):
|
||||||
"""Update minutely data."""
|
"""Update minutely data."""
|
||||||
self.data_minutely = self.data.minutely()
|
self.data_minutely = self.data and self.data.minutely()
|
||||||
|
|
||||||
def _update_hourly(self):
|
def _update_hourly(self):
|
||||||
"""Update hourly data."""
|
"""Update hourly data."""
|
||||||
self.data_hourly = self.data.hourly()
|
self.data_hourly = self.data and self.data.hourly()
|
||||||
|
|
||||||
def _update_daily(self):
|
def _update_daily(self):
|
||||||
"""Update daily data."""
|
"""Update daily data."""
|
||||||
self.data_daily = self.data.daily()
|
self.data_daily = self.data and self.data.daily()
|
||||||
|
@ -26,15 +26,15 @@ stores/caches the latest telegram and notifies the Entities that the telegram
|
|||||||
has been updated.
|
has been updated.
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import (
|
||||||
|
CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -65,30 +65,37 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||||||
# Suppress logging
|
# Suppress logging
|
||||||
logging.getLogger('dsmr_parser').setLevel(logging.ERROR)
|
logging.getLogger('dsmr_parser').setLevel(logging.ERROR)
|
||||||
|
|
||||||
from dsmr_parser import obis_references as obis
|
from dsmr_parser import obis_references as obis_ref
|
||||||
from dsmr_parser.protocol import create_dsmr_reader
|
from dsmr_parser.protocol import create_dsmr_reader
|
||||||
|
|
||||||
dsmr_version = config[CONF_DSMR_VERSION]
|
dsmr_version = config[CONF_DSMR_VERSION]
|
||||||
|
|
||||||
# Define list of name,obis mappings to generate entities
|
# Define list of name,obis mappings to generate entities
|
||||||
obis_mapping = [
|
obis_mapping = [
|
||||||
['Power Consumption', obis.CURRENT_ELECTRICITY_USAGE],
|
['Power Consumption', obis_ref.CURRENT_ELECTRICITY_USAGE],
|
||||||
['Power Production', obis.CURRENT_ELECTRICITY_DELIVERY],
|
['Power Production', obis_ref.CURRENT_ELECTRICITY_DELIVERY],
|
||||||
['Power Tariff', obis.ELECTRICITY_ACTIVE_TARIFF],
|
['Power Tariff', obis_ref.ELECTRICITY_ACTIVE_TARIFF],
|
||||||
['Power Consumption (low)', obis.ELECTRICITY_USED_TARIFF_1],
|
['Power Consumption (low)', obis_ref.ELECTRICITY_USED_TARIFF_1],
|
||||||
['Power Consumption (normal)', obis.ELECTRICITY_USED_TARIFF_2],
|
['Power Consumption (normal)', obis_ref.ELECTRICITY_USED_TARIFF_2],
|
||||||
['Power Production (low)', obis.ELECTRICITY_DELIVERED_TARIFF_1],
|
['Power Production (low)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_1],
|
||||||
['Power Production (normal)', obis.ELECTRICITY_DELIVERED_TARIFF_2],
|
['Power Production (normal)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_2],
|
||||||
]
|
]
|
||||||
# Protocol version specific obis
|
|
||||||
if dsmr_version == '4':
|
|
||||||
obis_mapping.append(['Gas Consumption', obis.HOURLY_GAS_METER_READING])
|
|
||||||
else:
|
|
||||||
obis_mapping.append(['Gas Consumption', obis.GAS_METER_READING])
|
|
||||||
|
|
||||||
# Generate device entities
|
# Generate device entities
|
||||||
devices = [DSMREntity(name, obis) for name, obis in obis_mapping]
|
devices = [DSMREntity(name, obis) for name, obis in obis_mapping]
|
||||||
|
|
||||||
|
# Protocol version specific obis
|
||||||
|
if dsmr_version == '4':
|
||||||
|
gas_obis = obis_ref.HOURLY_GAS_METER_READING
|
||||||
|
else:
|
||||||
|
gas_obis = obis_ref.GAS_METER_READING
|
||||||
|
|
||||||
|
# add gas meter reading and derivative for usage
|
||||||
|
devices += [
|
||||||
|
DSMREntity('Gas Consumption', gas_obis),
|
||||||
|
DerivativeDSMREntity('Hourly Gas Consumption', gas_obis),
|
||||||
|
]
|
||||||
|
|
||||||
yield from async_add_devices(devices)
|
yield from async_add_devices(devices)
|
||||||
|
|
||||||
def update_entities_telegram(telegram):
|
def update_entities_telegram(telegram):
|
||||||
@ -151,7 +158,10 @@ class DSMREntity(Entity):
|
|||||||
if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF:
|
if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF:
|
||||||
return self.translate_tariff(value)
|
return self.translate_tariff(value)
|
||||||
else:
|
else:
|
||||||
return value
|
if value:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
@ -168,4 +178,55 @@ class DSMREntity(Entity):
|
|||||||
elif value == '0001':
|
elif value == '0001':
|
||||||
return 'low'
|
return 'low'
|
||||||
else:
|
else:
|
||||||
return None
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
|
class DerivativeDSMREntity(DSMREntity):
|
||||||
|
"""Calculated derivative for values where the DSMR doesn't offer one.
|
||||||
|
|
||||||
|
Gas readings are only reported per hour and don't offer a rate only
|
||||||
|
the current meter reading. This entity converts subsequents readings
|
||||||
|
into a hourly rate.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_previous_reading = None
|
||||||
|
_previous_timestamp = None
|
||||||
|
_state = STATE_UNKNOWN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the calculated current hourly rate."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def async_update(self):
|
||||||
|
"""Recalculate hourly rate if timestamp has changed.
|
||||||
|
|
||||||
|
DSMR updates gas meter reading every hour. Along with the
|
||||||
|
new value a timestamp is provided for the reading. Test
|
||||||
|
if the last known timestamp differs from the current one
|
||||||
|
then calculate a new rate for the previous hour.
|
||||||
|
"""
|
||||||
|
# check if the timestamp for the object differs from the previous one
|
||||||
|
timestamp = self.get_dsmr_object_attr('datetime')
|
||||||
|
if timestamp and timestamp != self._previous_timestamp:
|
||||||
|
current_reading = self.get_dsmr_object_attr('value')
|
||||||
|
|
||||||
|
if self._previous_reading is None:
|
||||||
|
# can't calculate rate without previous datapoint
|
||||||
|
# just store current point
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# recalculate the rate
|
||||||
|
diff = current_reading - self._previous_reading
|
||||||
|
self._state = diff
|
||||||
|
|
||||||
|
self._previous_reading = current_reading
|
||||||
|
self._previous_timestamp = timestamp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit of measurement of this entity, per hour, if any."""
|
||||||
|
unit = self.get_dsmr_object_attr('unit')
|
||||||
|
if unit:
|
||||||
|
return unit + '/h'
|
||||||
|
@ -14,7 +14,7 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN)
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['eliqonline==1.0.12']
|
REQUIREMENTS = ['eliqonline==1.0.13']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ CONF_SECOND = 'second'
|
|||||||
CONF_MINUTE = 'minute'
|
CONF_MINUTE = 'minute'
|
||||||
CONF_HOUR = 'hour'
|
CONF_HOUR = 'hour'
|
||||||
CONF_DAY = 'day'
|
CONF_DAY = 'day'
|
||||||
|
CONF_MANUAL = 'manual'
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_SECOND, default=[0]):
|
vol.Optional(CONF_SECOND, default=[0]):
|
||||||
@ -32,6 +33,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]),
|
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]),
|
||||||
vol.Optional(CONF_DAY):
|
vol.Optional(CONF_DAY):
|
||||||
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]),
|
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]),
|
||||||
|
vol.Optional(CONF_MANUAL, default=False): cv.boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -104,11 +106,12 @@ class SpeedtestData(object):
|
|||||||
def __init__(self, hass, config):
|
def __init__(self, hass, config):
|
||||||
"""Initialize the data object."""
|
"""Initialize the data object."""
|
||||||
self.data = None
|
self.data = None
|
||||||
track_time_change(hass, self.update,
|
if not config.get(CONF_MANUAL):
|
||||||
second=config.get(CONF_SECOND),
|
track_time_change(hass, self.update,
|
||||||
minute=config.get(CONF_MINUTE),
|
second=config.get(CONF_SECOND),
|
||||||
hour=config.get(CONF_HOUR),
|
minute=config.get(CONF_MINUTE),
|
||||||
day=config.get(CONF_DAY))
|
hour=config.get(CONF_HOUR),
|
||||||
|
day=config.get(CONF_DAY))
|
||||||
|
|
||||||
def update(self, now):
|
def update(self, now):
|
||||||
"""Get the latest data from fast.com."""
|
"""Get the latest data from fast.com."""
|
||||||
|
@ -95,6 +95,9 @@ def get_next_departure(sched, start_station_id, end_station_id):
|
|||||||
for row in result:
|
for row in result:
|
||||||
item = row
|
item = row
|
||||||
|
|
||||||
|
if item == {}:
|
||||||
|
return None
|
||||||
|
|
||||||
today = datetime.datetime.today().strftime('%Y-%m-%d')
|
today = datetime.datetime.today().strftime('%Y-%m-%d')
|
||||||
departure_time_string = '{} {}'.format(today, item[2])
|
departure_time_string = '{} {}'.format(today, item[2])
|
||||||
arrival_time_string = '{} {}'.format(today, item[3])
|
arrival_time_string = '{} {}'.format(today, item[3])
|
||||||
@ -221,6 +224,13 @@ class GTFSDepartureSensor(Entity):
|
|||||||
with self.lock:
|
with self.lock:
|
||||||
self._departure = get_next_departure(self._pygtfs, self.origin,
|
self._departure = get_next_departure(self._pygtfs, self.origin,
|
||||||
self.destination)
|
self.destination)
|
||||||
|
if not self._departure:
|
||||||
|
self._state = 0
|
||||||
|
self._attributes = {'Info': 'No more bus today'}
|
||||||
|
if self._name == '':
|
||||||
|
self._name = (self._custom_name or "GTFS Sensor")
|
||||||
|
return
|
||||||
|
|
||||||
self._state = self._departure['minutes_until_departure']
|
self._state = self._departure['minutes_until_departure']
|
||||||
|
|
||||||
origin_station = self._departure['origin_station']
|
origin_station = self._departure['origin_station']
|
||||||
|
@ -18,7 +18,8 @@ from homeassistant.const import (
|
|||||||
|
|
||||||
DEPENDENCIES = ['nest']
|
DEPENDENCIES = ['nest']
|
||||||
SENSOR_TYPES = ['humidity',
|
SENSOR_TYPES = ['humidity',
|
||||||
'operation_mode']
|
'operation_mode',
|
||||||
|
'hvac_state']
|
||||||
|
|
||||||
SENSOR_TYPES_DEPRECATED = ['last_ip',
|
SENSOR_TYPES_DEPRECATED = ['last_ip',
|
||||||
'local_ip',
|
'local_ip',
|
||||||
|
@ -38,14 +38,19 @@ SENSOR_TYPES = {
|
|||||||
'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'],
|
'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'],
|
||||||
'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'],
|
'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'],
|
||||||
'battery_vp': ['Battery', '', 'mdi:battery'],
|
'battery_vp': ['Battery', '', 'mdi:battery'],
|
||||||
|
'battery_lvl': ['Battery_lvl', '', 'mdi:battery'],
|
||||||
'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer'],
|
'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer'],
|
||||||
'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer'],
|
'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer'],
|
||||||
'WindAngle': ['Angle', '', 'mdi:compass'],
|
'WindAngle': ['Angle', '', 'mdi:compass'],
|
||||||
|
'WindAngle_value': ['Angle Value', 'º', 'mdi:compass'],
|
||||||
'WindStrength': ['Strength', 'km/h', 'mdi:weather-windy'],
|
'WindStrength': ['Strength', 'km/h', 'mdi:weather-windy'],
|
||||||
'GustAngle': ['Gust Angle', '', 'mdi:compass'],
|
'GustAngle': ['Gust Angle', '', 'mdi:compass'],
|
||||||
|
'GustAngle_value': ['Gust Angle Value', 'º', 'mdi:compass'],
|
||||||
'GustStrength': ['Gust Strength', 'km/h', 'mdi:weather-windy'],
|
'GustStrength': ['Gust Strength', 'km/h', 'mdi:weather-windy'],
|
||||||
'rf_status': ['Radio', '', 'mdi:signal'],
|
'rf_status': ['Radio', '', 'mdi:signal'],
|
||||||
'wifi_status': ['Wifi', '', 'mdi:wifi']
|
'rf_status_lvl': ['Radio_lvl', '', 'mdi:signal'],
|
||||||
|
'wifi_status': ['Wifi', '', 'mdi:wifi'],
|
||||||
|
'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi']
|
||||||
}
|
}
|
||||||
|
|
||||||
MODULE_SCHEMA = vol.Schema({
|
MODULE_SCHEMA = vol.Schema({
|
||||||
@ -103,6 +108,7 @@ class NetAtmoSensor(Entity):
|
|||||||
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
||||||
module_id = self.netatmo_data.\
|
module_id = self.netatmo_data.\
|
||||||
station_data.moduleByName(module=module_name)['_id']
|
station_data.moduleByName(module=module_name)['_id']
|
||||||
|
self.module_id = module_id[1]
|
||||||
self._unique_id = "Netatmo Sensor {0} - {1} ({2})".format(self._name,
|
self._unique_id = "Netatmo Sensor {0} - {1} ({2})".format(self._name,
|
||||||
module_id,
|
module_id,
|
||||||
self.type)
|
self.type)
|
||||||
@ -154,21 +160,58 @@ class NetAtmoSensor(Entity):
|
|||||||
self._state = data['CO2']
|
self._state = data['CO2']
|
||||||
elif self.type == 'pressure':
|
elif self.type == 'pressure':
|
||||||
self._state = round(data['Pressure'], 1)
|
self._state = round(data['Pressure'], 1)
|
||||||
elif self.type == 'battery_vp':
|
elif self.type == 'battery_lvl':
|
||||||
|
self._state = data['battery_vp']
|
||||||
|
elif self.type == 'battery_vp' and self.module_id == '6':
|
||||||
|
if data['battery_vp'] >= 5590:
|
||||||
|
self._state = "Full"
|
||||||
|
elif data['battery_vp'] >= 5180:
|
||||||
|
self._state = "High"
|
||||||
|
elif data['battery_vp'] >= 4770:
|
||||||
|
self._state = "Medium"
|
||||||
|
elif data['battery_vp'] >= 4360:
|
||||||
|
self._state = "Low"
|
||||||
|
elif data['battery_vp'] < 4360:
|
||||||
|
self._state = "Very Low"
|
||||||
|
elif self.type == 'battery_vp' and self.module_id == '5':
|
||||||
if data['battery_vp'] >= 5500:
|
if data['battery_vp'] >= 5500:
|
||||||
self._state = "Full"
|
self._state = "Full"
|
||||||
elif data['battery_vp'] >= 5100:
|
elif data['battery_vp'] >= 5000:
|
||||||
self._state = "High"
|
self._state = "High"
|
||||||
elif data['battery_vp'] >= 4600:
|
elif data['battery_vp'] >= 4500:
|
||||||
self._state = "Medium"
|
self._state = "Medium"
|
||||||
elif data['battery_vp'] >= 4100:
|
elif data['battery_vp'] >= 4000:
|
||||||
self._state = "Low"
|
self._state = "Low"
|
||||||
elif data['battery_vp'] < 4100:
|
elif data['battery_vp'] < 4000:
|
||||||
|
self._state = "Very Low"
|
||||||
|
elif self.type == 'battery_vp' and self.module_id == '3':
|
||||||
|
if data['battery_vp'] >= 5640:
|
||||||
|
self._state = "Full"
|
||||||
|
elif data['battery_vp'] >= 5280:
|
||||||
|
self._state = "High"
|
||||||
|
elif data['battery_vp'] >= 4920:
|
||||||
|
self._state = "Medium"
|
||||||
|
elif data['battery_vp'] >= 4560:
|
||||||
|
self._state = "Low"
|
||||||
|
elif data['battery_vp'] < 4560:
|
||||||
|
self._state = "Very Low"
|
||||||
|
elif self.type == 'battery_vp' and self.module_id == '2':
|
||||||
|
if data['battery_vp'] >= 5500:
|
||||||
|
self._state = "Full"
|
||||||
|
elif data['battery_vp'] >= 5000:
|
||||||
|
self._state = "High"
|
||||||
|
elif data['battery_vp'] >= 4500:
|
||||||
|
self._state = "Medium"
|
||||||
|
elif data['battery_vp'] >= 4000:
|
||||||
|
self._state = "Low"
|
||||||
|
elif data['battery_vp'] < 4000:
|
||||||
self._state = "Very Low"
|
self._state = "Very Low"
|
||||||
elif self.type == 'min_temp':
|
elif self.type == 'min_temp':
|
||||||
self._state = data['min_temp']
|
self._state = data['min_temp']
|
||||||
elif self.type == 'max_temp':
|
elif self.type == 'max_temp':
|
||||||
self._state = data['max_temp']
|
self._state = data['max_temp']
|
||||||
|
elif self.type == 'WindAngle_value':
|
||||||
|
self._state = data['WindAngle']
|
||||||
elif self.type == 'WindAngle':
|
elif self.type == 'WindAngle':
|
||||||
if data['WindAngle'] >= 330:
|
if data['WindAngle'] >= 330:
|
||||||
self._state = "North (%d\xb0)" % data['WindAngle']
|
self._state = "North (%d\xb0)" % data['WindAngle']
|
||||||
@ -190,6 +233,8 @@ class NetAtmoSensor(Entity):
|
|||||||
self._state = "North (%d\xb0)" % data['WindAngle']
|
self._state = "North (%d\xb0)" % data['WindAngle']
|
||||||
elif self.type == 'WindStrength':
|
elif self.type == 'WindStrength':
|
||||||
self._state = data['WindStrength']
|
self._state = data['WindStrength']
|
||||||
|
elif self.type == 'GustAngle_value':
|
||||||
|
self._state = data['GustAngle']
|
||||||
elif self.type == 'GustAngle':
|
elif self.type == 'GustAngle':
|
||||||
if data['GustAngle'] >= 330:
|
if data['GustAngle'] >= 330:
|
||||||
self._state = "North (%d\xb0)" % data['GustAngle']
|
self._state = "North (%d\xb0)" % data['GustAngle']
|
||||||
@ -211,6 +256,8 @@ class NetAtmoSensor(Entity):
|
|||||||
self._state = "North (%d\xb0)" % data['GustAngle']
|
self._state = "North (%d\xb0)" % data['GustAngle']
|
||||||
elif self.type == 'GustStrength':
|
elif self.type == 'GustStrength':
|
||||||
self._state = data['GustStrength']
|
self._state = data['GustStrength']
|
||||||
|
elif self.type == 'rf_status_lvl':
|
||||||
|
self._state = data['rf_status']
|
||||||
elif self.type == 'rf_status':
|
elif self.type == 'rf_status':
|
||||||
if data['rf_status'] >= 90:
|
if data['rf_status'] >= 90:
|
||||||
self._state = "Low"
|
self._state = "Low"
|
||||||
@ -220,13 +267,17 @@ class NetAtmoSensor(Entity):
|
|||||||
self._state = "High"
|
self._state = "High"
|
||||||
elif data['rf_status'] <= 59:
|
elif data['rf_status'] <= 59:
|
||||||
self._state = "Full"
|
self._state = "Full"
|
||||||
|
elif self.type == 'wifi_status_lvl':
|
||||||
|
self._state = data['wifi_status']
|
||||||
elif self.type == 'wifi_status':
|
elif self.type == 'wifi_status':
|
||||||
if data['wifi_status'] >= 86:
|
if data['wifi_status'] >= 86:
|
||||||
self._state = "Bad"
|
self._state = "Low"
|
||||||
elif data['wifi_status'] >= 71:
|
elif data['wifi_status'] >= 71:
|
||||||
self._state = "Middle"
|
self._state = "Medium"
|
||||||
elif data['wifi_status'] <= 70:
|
elif data['wifi_status'] >= 56:
|
||||||
self._state = "Good"
|
self._state = "High"
|
||||||
|
elif data['wifi_status'] <= 55:
|
||||||
|
self._state = "Full"
|
||||||
|
|
||||||
|
|
||||||
class NetAtmoData(object):
|
class NetAtmoData(object):
|
||||||
@ -248,7 +299,7 @@ class NetAtmoData(object):
|
|||||||
def update(self):
|
def update(self):
|
||||||
"""Call the Netatmo API to update the data."""
|
"""Call the Netatmo API to update the data."""
|
||||||
import lnetatmo
|
import lnetatmo
|
||||||
self.station_data = lnetatmo.DeviceList(self.auth)
|
self.station_data = lnetatmo.WeatherStationData(self.auth)
|
||||||
|
|
||||||
if self.station is not None:
|
if self.station is not None:
|
||||||
self.data = self.station_data.lastData(station=self.station,
|
self.data = self.station_data.lastData(station=self.station,
|
||||||
|
147
homeassistant/components/sensor/netdata.py
Normal file
147
homeassistant/components/sensor/netdata.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"""
|
||||||
|
Support gathering system information of hosts which are running netdata.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.netdata/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST, CONF_PORT, STATE_UNKNOWN, CONF_NAME, CONF_RESOURCES)
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
_RESOURCE = 'api/v1'
|
||||||
|
_REALTIME = 'before=0&after=-1&options=seconds'
|
||||||
|
|
||||||
|
DEFAULT_HOST = 'localhost'
|
||||||
|
DEFAULT_NAME = 'Netdata'
|
||||||
|
DEFAULT_PORT = '19999'
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||||
|
|
||||||
|
SENSOR_TYPES = {
|
||||||
|
'memory_free': ['RAM Free', 'MiB', 'system.ram', 'free', 1],
|
||||||
|
'memory_used': ['RAM Used', 'MiB', 'system.ram', 'used', 1],
|
||||||
|
'memory_cached': ['RAM Cached', 'MiB', 'system.ram', 'cached', 1],
|
||||||
|
'memory_buffers': ['RAM Buffers', 'MiB', 'system.ram', 'buffers', 1],
|
||||||
|
'swap_free': ['Swap Free', 'MiB', 'system.swap', 'free', 1],
|
||||||
|
'swap_used': ['Swap Used', 'MiB', 'system.swap', 'used', 1],
|
||||||
|
'processes_running': ['Processes Running', 'Count', 'system.processes',
|
||||||
|
'running', 0],
|
||||||
|
'processes_blocked': ['Processes Blocked', 'Count', 'system.processes',
|
||||||
|
'blocked', 0],
|
||||||
|
'system_load': ['System Load', '15 min', 'system.processes', 'running', 2],
|
||||||
|
'system_io_in': ['System IO In', 'Count', 'system.io', 'in', 0],
|
||||||
|
'system_io_out': ['System IO Out', 'Count', 'system.io', 'out', 0],
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
vol.Optional(CONF_RESOURCES, default=['memory_free']):
|
||||||
|
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-variable
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Set up the Netdata sensor."""
|
||||||
|
name = config.get(CONF_NAME)
|
||||||
|
host = config.get(CONF_HOST)
|
||||||
|
port = config.get(CONF_PORT)
|
||||||
|
url = 'http://{}:{}'.format(host, port)
|
||||||
|
version_url = '{}/version.txt'.format(url)
|
||||||
|
data_url = '{}/{}/data?chart='.format(url, _RESOURCE)
|
||||||
|
resources = config.get(CONF_RESOURCES)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(version_url, timeout=10)
|
||||||
|
if not response.ok:
|
||||||
|
_LOGGER.error("Response status is '%s'", response.status_code)
|
||||||
|
return False
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
_LOGGER.error("No route to resource/endpoint: %s", url)
|
||||||
|
return False
|
||||||
|
|
||||||
|
values = {}
|
||||||
|
for key, value in sorted(SENSOR_TYPES.items()):
|
||||||
|
if key in resources:
|
||||||
|
values.setdefault(value[2], []).append(key)
|
||||||
|
|
||||||
|
dev = []
|
||||||
|
for chart in values:
|
||||||
|
rest_url = '{}{}&{}'.format(data_url, chart, _REALTIME)
|
||||||
|
rest = NetdataData(rest_url)
|
||||||
|
for sensor_type in values[chart]:
|
||||||
|
dev.append(NetdataSensor(rest, name, sensor_type))
|
||||||
|
|
||||||
|
add_devices(dev)
|
||||||
|
|
||||||
|
|
||||||
|
class NetdataSensor(Entity):
|
||||||
|
"""Implementation of a Netdata sensor."""
|
||||||
|
|
||||||
|
def __init__(self, rest, name, sensor_type):
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.rest = rest
|
||||||
|
self.type = sensor_type
|
||||||
|
self._name = '{} {}'.format(name, SENSOR_TYPES[self.type][0])
|
||||||
|
self._precision = SENSOR_TYPES[self.type][4]
|
||||||
|
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""The name of the sensor."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit the value is expressed in."""
|
||||||
|
return self._unit_of_measurement
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the resources."""
|
||||||
|
value = self.rest.data
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
netdata_id = SENSOR_TYPES[self.type][3]
|
||||||
|
if netdata_id in value:
|
||||||
|
return "{0:.{1}f}".format(value[netdata_id], self._precision)
|
||||||
|
else:
|
||||||
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest data from Netdata REST API."""
|
||||||
|
self.rest.update()
|
||||||
|
|
||||||
|
|
||||||
|
class NetdataData(object):
|
||||||
|
"""The class for handling the data retrieval."""
|
||||||
|
|
||||||
|
def __init__(self, resource):
|
||||||
|
"""Initialize the data object."""
|
||||||
|
self._resource = resource
|
||||||
|
self.data = None
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest data from the Netdata REST API."""
|
||||||
|
try:
|
||||||
|
response = requests.get(self._resource, timeout=5)
|
||||||
|
det = response.json()
|
||||||
|
self.data = {k: v for k, v in zip(det['labels'], det['data'][0])}
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
_LOGGER.error("No route to host/endpoint: %s", self._resource)
|
||||||
|
self.data = None
|
134
homeassistant/components/sensor/sensehat.py
Normal file
134
homeassistant/components/sensor/sensehat.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
"""
|
||||||
|
Support for Sense HAT sensors.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.sensehat
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.const import (TEMP_CELSIUS, CONF_DISPLAY_OPTIONS, CONF_NAME)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
REQUIREMENTS = ['sense-hat==2.2.0']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'sensehat'
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||||
|
|
||||||
|
SENSOR_TYPES = {
|
||||||
|
'temperature': ['temperature', TEMP_CELSIUS],
|
||||||
|
'humidity': ['humidity', "%"],
|
||||||
|
'pressure': ['pressure', "mb"],
|
||||||
|
}
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_DISPLAY_OPTIONS, default=SENSOR_TYPES):
|
||||||
|
[vol.In(SENSOR_TYPES)],
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def get_cpu_temp():
|
||||||
|
"""Get CPU temperature."""
|
||||||
|
res = os.popen("vcgencmd measure_temp").readline()
|
||||||
|
t_cpu = float(res.replace("temp=", "").replace("'C\n", ""))
|
||||||
|
return t_cpu
|
||||||
|
|
||||||
|
|
||||||
|
def get_average(temp_base):
|
||||||
|
"""Use moving average to get better readings."""
|
||||||
|
if not hasattr(get_average, "temp"):
|
||||||
|
get_average.temp = [temp_base, temp_base, temp_base]
|
||||||
|
get_average.temp[2] = get_average.temp[1]
|
||||||
|
get_average.temp[1] = get_average.temp[0]
|
||||||
|
get_average.temp[0] = temp_base
|
||||||
|
temp_avg = (get_average.temp[0]+get_average.temp[1]+get_average.temp[2])/3
|
||||||
|
return temp_avg
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup the sensor platform."""
|
||||||
|
data = SenseHatData()
|
||||||
|
dev = []
|
||||||
|
|
||||||
|
for variable in config[CONF_DISPLAY_OPTIONS]:
|
||||||
|
dev.append(SenseHatSensor(data, variable))
|
||||||
|
|
||||||
|
add_devices(dev)
|
||||||
|
|
||||||
|
|
||||||
|
class SenseHatSensor(Entity):
|
||||||
|
"""Representation of a sensehat sensor."""
|
||||||
|
|
||||||
|
def __init__(self, data, sensor_types):
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.data = data
|
||||||
|
self._name = SENSOR_TYPES[sensor_types][0]
|
||||||
|
self._unit_of_measurement = SENSOR_TYPES[sensor_types][1]
|
||||||
|
self.type = sensor_types
|
||||||
|
self._state = None
|
||||||
|
"""updating data."""
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit the value is expressed in."""
|
||||||
|
return self._unit_of_measurement
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest data and updates the states."""
|
||||||
|
self.data.update()
|
||||||
|
if not self.data.humidity:
|
||||||
|
_LOGGER.error("Don't receive data!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.type == 'temperature':
|
||||||
|
self._state = self.data.temperature
|
||||||
|
if self.type == 'humidity':
|
||||||
|
self._state = self.data.humidity
|
||||||
|
if self.type == 'pressure':
|
||||||
|
self._state = self.data.pressure
|
||||||
|
|
||||||
|
|
||||||
|
class SenseHatData(object):
|
||||||
|
"""Get the latest data and update."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the data object."""
|
||||||
|
self.temperature = None
|
||||||
|
self.humidity = None
|
||||||
|
self.pressure = None
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest data from sensehat."""
|
||||||
|
from sense_hat import SenseHat
|
||||||
|
sense = SenseHat()
|
||||||
|
temp_from_h = sense.get_temperature_from_humidity()
|
||||||
|
temp_from_p = sense.get_temperature_from_pressure()
|
||||||
|
t_cpu = get_cpu_temp()
|
||||||
|
t_total = (temp_from_h+temp_from_p)/2
|
||||||
|
t_correct = t_total - ((t_cpu-t_total)/1.5)
|
||||||
|
t_correct = get_average(t_correct)
|
||||||
|
self.temperature = t_correct
|
||||||
|
self.humidity = sense.get_humidity()
|
||||||
|
self.pressure = sense.get_pressure()
|
@ -31,6 +31,7 @@ CONF_MINUTE = 'minute'
|
|||||||
CONF_HOUR = 'hour'
|
CONF_HOUR = 'hour'
|
||||||
CONF_DAY = 'day'
|
CONF_DAY = 'day'
|
||||||
CONF_SERVER_ID = 'server_id'
|
CONF_SERVER_ID = 'server_id'
|
||||||
|
CONF_MANUAL = 'manual'
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
'ping': ['Ping', 'ms'],
|
'ping': ['Ping', 'ms'],
|
||||||
@ -50,6 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||||||
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]),
|
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]),
|
||||||
vol.Optional(CONF_DAY):
|
vol.Optional(CONF_DAY):
|
||||||
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]),
|
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]),
|
||||||
|
vol.Optional(CONF_MANUAL, default=False): cv.boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -135,11 +137,12 @@ class SpeedtestData(object):
|
|||||||
"""Initialize the data object."""
|
"""Initialize the data object."""
|
||||||
self.data = None
|
self.data = None
|
||||||
self._server_id = config.get(CONF_SERVER_ID)
|
self._server_id = config.get(CONF_SERVER_ID)
|
||||||
track_time_change(hass, self.update,
|
if not config.get(CONF_MANUAL):
|
||||||
second=config.get(CONF_SECOND),
|
track_time_change(hass, self.update,
|
||||||
minute=config.get(CONF_MINUTE),
|
second=config.get(CONF_SECOND),
|
||||||
hour=config.get(CONF_HOUR),
|
minute=config.get(CONF_MINUTE),
|
||||||
day=config.get(CONF_DAY))
|
hour=config.get(CONF_HOUR),
|
||||||
|
day=config.get(CONF_DAY))
|
||||||
|
|
||||||
def update(self, now):
|
def update(self, now):
|
||||||
"""Get the latest data from speedtest.net."""
|
"""Get the latest data from speedtest.net."""
|
||||||
|
@ -107,7 +107,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
# Handle all Volumes
|
# Handle all Volumes
|
||||||
volumes = config['volumes']
|
volumes = config['volumes']
|
||||||
if volumes is None:
|
if volumes is None:
|
||||||
volumes = api.storage().volumes
|
volumes = api.storage.volumes
|
||||||
|
|
||||||
for volume in volumes:
|
for volume in volumes:
|
||||||
sensors += [SynoNasStorageSensor(api, variable,
|
sensors += [SynoNasStorageSensor(api, variable,
|
||||||
@ -119,7 +119,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
# Handle all Disks
|
# Handle all Disks
|
||||||
disks = config['disks']
|
disks = config['disks']
|
||||||
if disks is None:
|
if disks is None:
|
||||||
disks = api.storage().disks
|
disks = api.storage.disks
|
||||||
|
|
||||||
for disk in disks:
|
for disk in disks:
|
||||||
sensors += [SynoNasStorageSensor(api, variable,
|
sensors += [SynoNasStorageSensor(api, variable,
|
||||||
|
@ -27,14 +27,14 @@ SENSOR_TYPES = {
|
|||||||
'memory_use': ['RAM Use', 'MiB', 'mdi:memory'],
|
'memory_use': ['RAM Use', 'MiB', 'mdi:memory'],
|
||||||
'memory_free': ['RAM Free', 'MiB', 'mdi:memory'],
|
'memory_free': ['RAM Free', 'MiB', 'mdi:memory'],
|
||||||
'processor_use': ['CPU Use', '%', 'mdi:memory'],
|
'processor_use': ['CPU Use', '%', 'mdi:memory'],
|
||||||
'process': ['Process', '', 'mdi:memory'],
|
'process': ['Process', ' ', 'mdi:memory'],
|
||||||
'swap_use_percent': ['Swap Use', '%', 'mdi:harddisk'],
|
'swap_use_percent': ['Swap Use', '%', 'mdi:harddisk'],
|
||||||
'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'],
|
'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'],
|
||||||
'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'],
|
'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'],
|
||||||
'network_out': ['Sent', 'MiB', 'mdi:server-network'],
|
'network_out': ['Sent', 'MiB', 'mdi:server-network'],
|
||||||
'network_in': ['Received', 'MiB', 'mdi:server-network'],
|
'network_in': ['Received', 'MiB', 'mdi:server-network'],
|
||||||
'packets_out': ['Packets sent', '', 'mdi:server-network'],
|
'packets_out': ['Packets sent', ' ', 'mdi:server-network'],
|
||||||
'packets_in': ['Packets received', '', 'mdi:server-network'],
|
'packets_in': ['Packets received', ' ', 'mdi:server-network'],
|
||||||
'ipv4_address': ['IPv4 address', '', 'mdi:server-network'],
|
'ipv4_address': ['IPv4 address', '', 'mdi:server-network'],
|
||||||
'ipv6_address': ['IPv6 address', '', 'mdi:server-network'],
|
'ipv6_address': ['IPv6 address', '', 'mdi:server-network'],
|
||||||
'last_boot': ['Last Boot', '', 'mdi:clock'],
|
'last_boot': ['Last Boot', '', 'mdi:clock'],
|
||||||
|
@ -6,37 +6,32 @@ https://home-assistant.io/components/sensor.tellduslive/
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from homeassistant.components import tellduslive
|
from homeassistant.components.tellduslive import TelldusLiveEntity
|
||||||
from homeassistant.const import (
|
from homeassistant.const import TEMP_CELSIUS
|
||||||
ATTR_BATTERY_LEVEL, DEVICE_DEFAULT_NAME, TEMP_CELSIUS)
|
|
||||||
from homeassistant.helpers.entity import Entity
|
|
||||||
|
|
||||||
ATTR_LAST_UPDATED = "time_last_updated"
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SENSOR_TYPE_TEMP = "temp"
|
SENSOR_TYPE_TEMP = 'temp'
|
||||||
SENSOR_TYPE_HUMIDITY = "humidity"
|
SENSOR_TYPE_HUMIDITY = 'humidity'
|
||||||
SENSOR_TYPE_RAINRATE = "rrate"
|
SENSOR_TYPE_RAINRATE = 'rrate'
|
||||||
SENSOR_TYPE_RAINTOTAL = "rtot"
|
SENSOR_TYPE_RAINTOTAL = 'rtot'
|
||||||
SENSOR_TYPE_WINDDIRECTION = "wdir"
|
SENSOR_TYPE_WINDDIRECTION = 'wdir'
|
||||||
SENSOR_TYPE_WINDAVERAGE = "wavg"
|
SENSOR_TYPE_WINDAVERAGE = 'wavg'
|
||||||
SENSOR_TYPE_WINDGUST = "wgust"
|
SENSOR_TYPE_WINDGUST = 'wgust'
|
||||||
SENSOR_TYPE_WATT = "watt"
|
SENSOR_TYPE_WATT = 'watt'
|
||||||
SENSOR_TYPE_LUMINANCE = "lum"
|
SENSOR_TYPE_LUMINANCE = 'lum'
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELSIUS, "mdi:thermometer"],
|
SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'],
|
||||||
SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"],
|
SENSOR_TYPE_HUMIDITY: ['Humidity', '%', 'mdi:water'],
|
||||||
SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"],
|
SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', 'mdi:water'],
|
||||||
SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"],
|
SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', 'mdi:water'],
|
||||||
SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""],
|
SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ''],
|
||||||
SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""],
|
SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ''],
|
||||||
SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""],
|
SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ''],
|
||||||
SENSOR_TYPE_WATT: ['Watt', 'W', ""],
|
SENSOR_TYPE_WATT: ['Watt', 'W', ''],
|
||||||
SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', ""],
|
SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', ''],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -44,114 +39,75 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
"""Setup Tellstick sensors."""
|
"""Setup Tellstick sensors."""
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
add_devices(TelldusLiveSensor(sensor) for sensor in discovery_info)
|
add_devices(TelldusLiveSensor(hass, sensor) for sensor in discovery_info)
|
||||||
|
|
||||||
|
|
||||||
class TelldusLiveSensor(Entity):
|
class TelldusLiveSensor(TelldusLiveEntity):
|
||||||
"""Representation of a Telldus Live sensor."""
|
"""Representation of a Telldus Live sensor."""
|
||||||
|
|
||||||
def __init__(self, sensor_id):
|
@property
|
||||||
"""Initialize the sensor."""
|
def device_id(self):
|
||||||
self._id = sensor_id
|
"""Return id of the device."""
|
||||||
self.update()
|
return self._id[0]
|
||||||
_LOGGER.debug("created sensor %s", self)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""Update sensor values."""
|
|
||||||
tellduslive.NETWORK.update_sensors()
|
|
||||||
self._sensor = tellduslive.NETWORK.get_sensor(self._id)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _sensor_name(self):
|
def _type(self):
|
||||||
"""Return the name of the sensor."""
|
|
||||||
return self._sensor["name"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _sensor_value(self):
|
|
||||||
"""Return the value the sensor."""
|
|
||||||
return self._sensor["data"]["value"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _sensor_type(self):
|
|
||||||
"""Return the type of the sensor."""
|
"""Return the type of the sensor."""
|
||||||
return self._sensor["data"]["name"]
|
return self._id[1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _battery_level(self):
|
def _value(self):
|
||||||
"""Return the battery level of a sensor."""
|
"""Return value of the sensor."""
|
||||||
sensor_battery_level = self._sensor.get("battery")
|
return self.device.value(self._id[1:])
|
||||||
return round(sensor_battery_level * 100 / 255) \
|
|
||||||
if sensor_battery_level else None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _last_updated(self):
|
|
||||||
"""Return the last update."""
|
|
||||||
sensor_last_updated = self._sensor.get("lastUpdated")
|
|
||||||
return str(datetime.fromtimestamp(sensor_last_updated)) \
|
|
||||||
if sensor_last_updated else None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _value_as_temperature(self):
|
def _value_as_temperature(self):
|
||||||
"""Return the value as temperature."""
|
"""Return the value as temperature."""
|
||||||
return round(float(self._sensor_value), 1)
|
return round(float(self._value), 1)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _value_as_luminance(self):
|
def _value_as_luminance(self):
|
||||||
"""Return the value as luminance."""
|
"""Return the value as luminance."""
|
||||||
return round(float(self._sensor_value), 1)
|
return round(float(self._value), 1)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _value_as_humidity(self):
|
def _value_as_humidity(self):
|
||||||
"""Return the value as humidity."""
|
"""Return the value as humidity."""
|
||||||
return int(round(float(self._sensor_value)))
|
return int(round(float(self._value)))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
return "{} {}".format(self._sensor_name or DEVICE_DEFAULT_NAME,
|
return '{} {}'.format(
|
||||||
self.quantity_name or "")
|
super().name,
|
||||||
|
self.quantity_name or '')
|
||||||
@property
|
|
||||||
def available(self):
|
|
||||||
"""Return true if the sensor is available."""
|
|
||||||
return not self._sensor.get("offline", False)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
if self._sensor_type == SENSOR_TYPE_TEMP:
|
if self._type == SENSOR_TYPE_TEMP:
|
||||||
return self._value_as_temperature
|
return self._value_as_temperature
|
||||||
elif self._sensor_type == SENSOR_TYPE_HUMIDITY:
|
elif self._type == SENSOR_TYPE_HUMIDITY:
|
||||||
return self._value_as_humidity
|
return self._value_as_humidity
|
||||||
elif self._sensor_type == SENSOR_TYPE_LUMINANCE:
|
elif self._type == SENSOR_TYPE_LUMINANCE:
|
||||||
return self._value_as_luminance
|
return self._value_as_luminance
|
||||||
else:
|
else:
|
||||||
return self._sensor_value
|
return self._value
|
||||||
|
|
||||||
@property
|
|
||||||
def device_state_attributes(self):
|
|
||||||
"""Return the state attributes."""
|
|
||||||
attrs = {}
|
|
||||||
if self._battery_level is not None:
|
|
||||||
attrs[ATTR_BATTERY_LEVEL] = self._battery_level
|
|
||||||
if self._last_updated is not None:
|
|
||||||
attrs[ATTR_LAST_UPDATED] = self._last_updated
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def quantity_name(self):
|
def quantity_name(self):
|
||||||
"""Name of quantity."""
|
"""Name of quantity."""
|
||||||
return SENSOR_TYPES[self._sensor_type][0] \
|
return SENSOR_TYPES[self._type][0] \
|
||||||
if self._sensor_type in SENSOR_TYPES else None
|
if self._type in SENSOR_TYPES else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
"""Return the unit of measurement."""
|
"""Return the unit of measurement."""
|
||||||
return SENSOR_TYPES[self._sensor_type][1] \
|
return SENSOR_TYPES[self._type][1] \
|
||||||
if self._sensor_type in SENSOR_TYPES else None
|
if self._type in SENSOR_TYPES else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self):
|
def icon(self):
|
||||||
"""Return the icon."""
|
"""Return the icon."""
|
||||||
return SENSOR_TYPES[self._sensor_type][2] \
|
return SENSOR_TYPES[self._type][2] \
|
||||||
if self._sensor_type in SENSOR_TYPES else None
|
if self._type in SENSOR_TYPES else None
|
||||||
|
@ -19,12 +19,15 @@ from homeassistant.helpers.entity import Entity
|
|||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_RESOURCE = 'http://api.wunderground.com/api/{}/conditions/q/'
|
_RESOURCE = 'http://api.wunderground.com/api/{}/conditions/{}/q/'
|
||||||
_ALERTS = 'http://api.wunderground.com/api/{}/alerts/q/'
|
_ALERTS = 'http://api.wunderground.com/api/{}/alerts/{}/q/'
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_ATTRIBUTION = "Data provided by the WUnderground weather service"
|
CONF_ATTRIBUTION = "Data provided by the WUnderground weather service"
|
||||||
CONF_PWS_ID = 'pws_id'
|
CONF_PWS_ID = 'pws_id'
|
||||||
|
CONF_LANG = 'lang'
|
||||||
|
|
||||||
|
DEFAULT_LANG = 'EN'
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES_ALERTS = timedelta(minutes=15)
|
MIN_TIME_BETWEEN_UPDATES_ALERTS = timedelta(minutes=15)
|
||||||
MIN_TIME_BETWEEN_UPDATES_OBSERVATION = timedelta(minutes=5)
|
MIN_TIME_BETWEEN_UPDATES_OBSERVATION = timedelta(minutes=5)
|
||||||
@ -80,9 +83,29 @@ ALERTS_ATTRS = [
|
|||||||
'message',
|
'message',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Language Supported Codes
|
||||||
|
LANG_CODES = [
|
||||||
|
'AF', 'AL', 'AR', 'HY', 'AZ', 'EU',
|
||||||
|
'BY', 'BU', 'LI', 'MY', 'CA', 'CN',
|
||||||
|
'TW', 'CR', 'CZ', 'DK', 'DV', 'NL',
|
||||||
|
'EN', 'EO', 'ET', 'FA', 'FI', 'FR',
|
||||||
|
'FC', 'GZ', 'DL', 'KA', 'GR', 'GU',
|
||||||
|
'HT', 'IL', 'HI', 'HU', 'IS', 'IO',
|
||||||
|
'ID', 'IR', 'IT', 'JP', 'JW', 'KM',
|
||||||
|
'KR', 'KU', 'LA', 'LV', 'LT', 'ND',
|
||||||
|
'MK', 'MT', 'GM', 'MI', 'MR', 'MN',
|
||||||
|
'NO', 'OC', 'PS', 'GN', 'PL', 'BR',
|
||||||
|
'PA', 'PU', 'RO', 'RU', 'SR', 'SK',
|
||||||
|
'SL', 'SP', 'SI', 'SW', 'CH', 'TL',
|
||||||
|
'TT', 'TH', 'UA', 'UZ', 'VU', 'CY',
|
||||||
|
'SN', 'JI', 'YI',
|
||||||
|
]
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_API_KEY): cv.string,
|
vol.Required(CONF_API_KEY): cv.string,
|
||||||
vol.Optional(CONF_PWS_ID): cv.string,
|
vol.Optional(CONF_PWS_ID): cv.string,
|
||||||
|
vol.Optional(CONF_LANG, default=DEFAULT_LANG):
|
||||||
|
vol.All(vol.In(LANG_CODES)),
|
||||||
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
|
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
|
||||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||||
})
|
})
|
||||||
@ -92,7 +115,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
"""Setup the WUnderground sensor."""
|
"""Setup the WUnderground sensor."""
|
||||||
rest = WUndergroundData(hass,
|
rest = WUndergroundData(hass,
|
||||||
config.get(CONF_API_KEY),
|
config.get(CONF_API_KEY),
|
||||||
config.get(CONF_PWS_ID, None))
|
config.get(CONF_PWS_ID),
|
||||||
|
config.get(CONF_LANG))
|
||||||
sensors = []
|
sensors = []
|
||||||
for variable in config[CONF_MONITORED_CONDITIONS]:
|
for variable in config[CONF_MONITORED_CONDITIONS]:
|
||||||
sensors.append(WUndergroundSensor(rest, variable))
|
sensors.append(WUndergroundSensor(rest, variable))
|
||||||
@ -172,7 +196,7 @@ class WUndergroundSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def entity_picture(self):
|
def entity_picture(self):
|
||||||
"""Return the entity picture."""
|
"""Return the entity picture."""
|
||||||
if self._condition == 'weather':
|
if self.rest.data and self._condition == 'weather':
|
||||||
url = self.rest.data['icon_url']
|
url = self.rest.data['icon_url']
|
||||||
return re.sub(r'^http://', 'https://', url, flags=re.IGNORECASE)
|
return re.sub(r'^http://', 'https://', url, flags=re.IGNORECASE)
|
||||||
|
|
||||||
@ -192,18 +216,19 @@ class WUndergroundSensor(Entity):
|
|||||||
class WUndergroundData(object):
|
class WUndergroundData(object):
|
||||||
"""Get data from WUnderground."""
|
"""Get data from WUnderground."""
|
||||||
|
|
||||||
def __init__(self, hass, api_key, pws_id=None):
|
def __init__(self, hass, api_key, pws_id, lang):
|
||||||
"""Initialize the data object."""
|
"""Initialize the data object."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._pws_id = pws_id
|
self._pws_id = pws_id
|
||||||
|
self._lang = 'lang:{}'.format(lang)
|
||||||
self._latitude = hass.config.latitude
|
self._latitude = hass.config.latitude
|
||||||
self._longitude = hass.config.longitude
|
self._longitude = hass.config.longitude
|
||||||
self.data = None
|
self.data = None
|
||||||
self.alerts = None
|
self.alerts = None
|
||||||
|
|
||||||
def _build_url(self, baseurl=_RESOURCE):
|
def _build_url(self, baseurl=_RESOURCE):
|
||||||
url = baseurl.format(self._api_key)
|
url = baseurl.format(self._api_key, self._lang)
|
||||||
if self._pws_id:
|
if self._pws_id:
|
||||||
url = url + 'pws:{}'.format(self._pws_id)
|
url = url + 'pws:{}'.format(self._pws_id)
|
||||||
else:
|
else:
|
||||||
|
178
homeassistant/components/sensor/zamg.py
Normal file
178
homeassistant/components/sensor/zamg.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
"""
|
||||||
|
Sensor for data from Austrian "Zentralanstalt für Meteorologie und Geodynamik".
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.zamg/
|
||||||
|
"""
|
||||||
|
import csv
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.weather import (
|
||||||
|
ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_PRESSURE,
|
||||||
|
ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING,
|
||||||
|
ATTR_WEATHER_WIND_SPEED)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_MONITORED_CONDITIONS, CONF_NAME, __version__)
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
ATTR_STATION = 'station'
|
||||||
|
ATTR_UPDATED = 'updated'
|
||||||
|
ATTRIBUTION = 'Data provided by ZAMG'
|
||||||
|
|
||||||
|
CONF_STATION_ID = 'station_id'
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'zamg'
|
||||||
|
|
||||||
|
# Data source only updates once per hour, so throttle to 30 min to have
|
||||||
|
# reasonably recent data
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30)
|
||||||
|
|
||||||
|
VALID_STATION_IDS = (
|
||||||
|
'11010', '11012', '11022', '11035', '11036', '11101', '11121', '11126',
|
||||||
|
'11130', '11150', '11155', '11157', '11171', '11190', '11204'
|
||||||
|
)
|
||||||
|
|
||||||
|
SENSOR_TYPES = {
|
||||||
|
ATTR_WEATHER_PRESSURE: ('Pressure', 'hPa', 'LDstat hPa', float),
|
||||||
|
'pressure_sealevel': ('Pressure at Sea Level', 'hPa', 'LDred hPa', float),
|
||||||
|
ATTR_WEATHER_HUMIDITY: ('Humidity', '%', 'RF %', int),
|
||||||
|
ATTR_WEATHER_WIND_SPEED: ('Wind Speed', 'km/h', 'WG km/h', float),
|
||||||
|
ATTR_WEATHER_WIND_BEARING: ('Wind Bearing', '°', 'WR °', int),
|
||||||
|
'wind_max_speed': ('Top Wind Speed', 'km/h', 'WSG km/h', float),
|
||||||
|
'wind_max_bearing': ('Top Wind Bearing', '°', 'WSR °', int),
|
||||||
|
'sun_last_hour': ('Sun Last Hour', '%', 'SO %', int),
|
||||||
|
ATTR_WEATHER_TEMPERATURE: ('Temperature', '°C', 'T °C', float),
|
||||||
|
'precipitation': ('Precipitation', 'l/m²', 'N l/m²', float),
|
||||||
|
'dewpoint': ('Dew Point', '°C', 'TP °C', float),
|
||||||
|
# The following probably not useful for general consumption,
|
||||||
|
# but we need them to fill in internal attributes
|
||||||
|
'station_name': ('Station Name', None, 'Name', str),
|
||||||
|
'station_elevation': ('Station Elevation', 'm', 'Höhe m', int),
|
||||||
|
'update_date': ('Update Date', None, 'Datum', str),
|
||||||
|
'update_time': ('Update Time', None, 'Zeit', str),
|
||||||
|
}
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_MONITORED_CONDITIONS):
|
||||||
|
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||||
|
vol.Required(CONF_STATION_ID):
|
||||||
|
vol.All(cv.string, vol.In(VALID_STATION_IDS)),
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Set up the ZAMG sensor platform."""
|
||||||
|
station_id = config.get(CONF_STATION_ID)
|
||||||
|
name = config.get(CONF_NAME)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
probe = ZamgData(station_id=station_id, logger=logger)
|
||||||
|
|
||||||
|
sensors = [ZamgSensor(probe, variable, name)
|
||||||
|
for variable in config[CONF_MONITORED_CONDITIONS]]
|
||||||
|
|
||||||
|
add_devices(sensors, True)
|
||||||
|
|
||||||
|
|
||||||
|
class ZamgSensor(Entity):
|
||||||
|
"""Implementation of a ZAMG sensor."""
|
||||||
|
|
||||||
|
def __init__(self, probe, variable, name):
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
self.probe = probe
|
||||||
|
self.client_name = name
|
||||||
|
self.variable = variable
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Delegate update to probe."""
|
||||||
|
self.probe.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
return '{} {}'.format(self.client_name, self.variable)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.probe.get_data(self.variable)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
"""Return the unit of measurement of this entity, if any."""
|
||||||
|
return SENSOR_TYPES[self.variable][1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
return {
|
||||||
|
ATTR_WEATHER_ATTRIBUTION: ATTRIBUTION,
|
||||||
|
ATTR_STATION: self.probe.get_data('station_name'),
|
||||||
|
ATTR_UPDATED: '{} {}'.format(self.probe.get_data('update_date'),
|
||||||
|
self.probe.get_data('update_time')),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ZamgData(object):
|
||||||
|
"""The class for handling the data retrieval."""
|
||||||
|
|
||||||
|
API_URL = 'http://www.zamg.ac.at/ogd/'
|
||||||
|
API_FIELDS = {
|
||||||
|
v[2]: (k, v[3])
|
||||||
|
for k, v in SENSOR_TYPES.items()
|
||||||
|
}
|
||||||
|
API_HEADERS = {
|
||||||
|
'User-Agent': '{} {}'.format('home-assistant.zamg/', __version__),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, logger, station_id):
|
||||||
|
"""Initialize the probe."""
|
||||||
|
self._logger = logger
|
||||||
|
self._station_id = station_id
|
||||||
|
self.data = {}
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest data from ZAMG."""
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
self.API_URL, headers=self.API_HEADERS, timeout=15)
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
|
self._logger.exception("While fetching data from server")
|
||||||
|
return
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
self._logger.error("API call returned with status %s",
|
||||||
|
response.status_code)
|
||||||
|
return
|
||||||
|
|
||||||
|
content_type = response.headers.get('Content-Type', 'whatever')
|
||||||
|
if content_type != 'text/csv':
|
||||||
|
self._logger.error("Expected text/csv but got %s", content_type)
|
||||||
|
return
|
||||||
|
|
||||||
|
response.encoding = 'UTF8'
|
||||||
|
content = response.text
|
||||||
|
data = (line for line in content.split('\n'))
|
||||||
|
reader = csv.DictReader(data, delimiter=';', quotechar='"')
|
||||||
|
for row in reader:
|
||||||
|
if row.get("Station", None) == self._station_id:
|
||||||
|
self.data = {
|
||||||
|
self.API_FIELDS.get(k)[0]:
|
||||||
|
self.API_FIELDS.get(k)[1](v.replace(',', '.'))
|
||||||
|
for k, v in row.items()
|
||||||
|
if v and k in self.API_FIELDS
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
def get_data(self, variable):
|
||||||
|
"""Generic accessor for data."""
|
||||||
|
return self.data.get(variable)
|
@ -144,3 +144,12 @@ openalpr:
|
|||||||
|
|
||||||
restart:
|
restart:
|
||||||
description: Restart ffmpeg process of device.
|
description: Restart ffmpeg process of device.
|
||||||
|
|
||||||
|
verisure:
|
||||||
|
capture_smartcam:
|
||||||
|
description: Capture a new image from a smartcam.
|
||||||
|
|
||||||
|
fields:
|
||||||
|
device_serial:
|
||||||
|
description: The serial number of the smartcam you want to capture an image from.
|
||||||
|
example: '2DEU AT5Z'
|
||||||
|
@ -17,8 +17,7 @@ from homeassistant.util import dt as dt_util
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
|
|
||||||
|
REQUIREMENTS = ['astral==1.3.3']
|
||||||
REQUIREMENTS = ['astral==1.3.2']
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
158
homeassistant/components/switch/broadlink.py
Normal file
158
homeassistant/components/switch/broadlink.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
"""
|
||||||
|
Support for Broadlink RM devices.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/switch.broadlink/
|
||||||
|
"""
|
||||||
|
from datetime import timedelta
|
||||||
|
from base64 import b64encode, b64decode
|
||||||
|
import asyncio
|
||||||
|
import binascii
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.loader as loader
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
|
||||||
|
from homeassistant.const import (CONF_FRIENDLY_NAME, CONF_SWITCHES,
|
||||||
|
CONF_COMMAND_OFF, CONF_COMMAND_ON,
|
||||||
|
CONF_TIMEOUT, CONF_HOST, CONF_MAC)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
REQUIREMENTS = ['broadlink==0.2']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOMAIN = "broadlink"
|
||||||
|
DEFAULT_NAME = 'Broadlink switch'
|
||||||
|
DEFAULT_TIMEOUT = 10
|
||||||
|
SERVICE_LEARN = "learn_command"
|
||||||
|
|
||||||
|
SWITCH_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CONF_COMMAND_OFF, default=None): cv.string,
|
||||||
|
vol.Optional(CONF_COMMAND_ON, default=None): cv.string,
|
||||||
|
vol.Optional(CONF_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}),
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Required(CONF_MAC): cv.string,
|
||||||
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Setup Broadlink switches."""
|
||||||
|
import broadlink
|
||||||
|
devices = config.get(CONF_SWITCHES, {})
|
||||||
|
switches = []
|
||||||
|
ip_addr = config.get(CONF_HOST)
|
||||||
|
mac_addr = binascii.unhexlify(
|
||||||
|
config.get(CONF_MAC).encode().replace(b':', b''))
|
||||||
|
broadlink_device = broadlink.rm((ip_addr, 80), mac_addr)
|
||||||
|
broadlink_device.timeout = config.get(CONF_TIMEOUT)
|
||||||
|
try:
|
||||||
|
broadlink_device.auth()
|
||||||
|
except socket.timeout:
|
||||||
|
_LOGGER.error("Failed to connect to device.")
|
||||||
|
|
||||||
|
persistent_notification = loader.get_component('persistent_notification')
|
||||||
|
|
||||||
|
@asyncio.coroutine
|
||||||
|
def _learn_command(call):
|
||||||
|
try:
|
||||||
|
yield from hass.loop.run_in_executor(None, broadlink_device.auth)
|
||||||
|
except socket.timeout:
|
||||||
|
_LOGGER.error("Failed to connect to device.")
|
||||||
|
return
|
||||||
|
yield from hass.loop.run_in_executor(None,
|
||||||
|
broadlink_device.enter_learning)
|
||||||
|
|
||||||
|
_LOGGER.info("Press the key you want HASS to learn")
|
||||||
|
start_time = utcnow()
|
||||||
|
while (utcnow() - start_time) < timedelta(seconds=20):
|
||||||
|
packet = yield from hass.loop.run_in_executor(None,
|
||||||
|
broadlink_device.
|
||||||
|
check_data)
|
||||||
|
if packet:
|
||||||
|
log_msg = 'Recieved packet is: {}'.\
|
||||||
|
format(b64encode(packet).decode('utf8'))
|
||||||
|
_LOGGER.info(log_msg)
|
||||||
|
persistent_notification.async_create(hass, log_msg,
|
||||||
|
title='Broadlink switch')
|
||||||
|
return
|
||||||
|
yield from asyncio.sleep(1, loop=hass.loop)
|
||||||
|
_LOGGER.error('Did not received any signal.')
|
||||||
|
persistent_notification.async_create(hass,
|
||||||
|
"Did not received any signal",
|
||||||
|
title='Broadlink switch')
|
||||||
|
hass.services.register(DOMAIN, SERVICE_LEARN, _learn_command)
|
||||||
|
|
||||||
|
for object_id, device_config in devices.items():
|
||||||
|
switches.append(
|
||||||
|
BroadlinkRM2Switch(
|
||||||
|
device_config.get(CONF_FRIENDLY_NAME, object_id),
|
||||||
|
device_config.get(CONF_COMMAND_ON),
|
||||||
|
device_config.get(CONF_COMMAND_OFF),
|
||||||
|
broadlink_device
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
add_devices(switches)
|
||||||
|
|
||||||
|
|
||||||
|
class BroadlinkRM2Switch(SwitchDevice):
|
||||||
|
"""Representation of an Broadlink switch."""
|
||||||
|
|
||||||
|
def __init__(self, friendly_name, command_on, command_off, device):
|
||||||
|
"""Initialize the switch."""
|
||||||
|
self._name = friendly_name
|
||||||
|
self._state = False
|
||||||
|
self._command_on = b64decode(command_on) if command_on else None
|
||||||
|
self._command_off = b64decode(command_off) if command_off else None
|
||||||
|
self._device = device
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the switch."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def assumed_state(self):
|
||||||
|
"""Return true if unable to access real state of entity."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if device is on."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Turn the device on."""
|
||||||
|
if self._sendpacket(self._command_on):
|
||||||
|
self._state = True
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Turn the device off."""
|
||||||
|
if self._sendpacket(self._command_off):
|
||||||
|
self._state = False
|
||||||
|
|
||||||
|
def _sendpacket(self, packet, retry=2):
|
||||||
|
"""Send packet to device."""
|
||||||
|
if packet is None:
|
||||||
|
_LOGGER.debug("Empty packet.")
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
self._device.send_data(packet)
|
||||||
|
except socket.timeout as error:
|
||||||
|
if retry < 1:
|
||||||
|
_LOGGER.error(error)
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
self._device.auth()
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
return self._sendpacket(packet, max(0, retry-1))
|
||||||
|
return True
|
148
homeassistant/components/switch/digitalloggers.py
Executable file
148
homeassistant/components/switch/digitalloggers.py
Executable file
@ -0,0 +1,148 @@
|
|||||||
|
"""
|
||||||
|
Support for Digital Loggers DIN III Relays.
|
||||||
|
|
||||||
|
Support for Digital Loggers DIN III Relays and possibly other items
|
||||||
|
through Dwight Hubbard's, python-dlipower.
|
||||||
|
|
||||||
|
For more details about python-dlipower, please see
|
||||||
|
https://github.com/dwighthubbard/python-dlipower
|
||||||
|
|
||||||
|
Custom ports are NOT supported due to a limitation of the dlipower
|
||||||
|
library, not the digital loggers switch
|
||||||
|
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT)
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
|
||||||
|
REQUIREMENTS = ['dlipower==0.7.165']
|
||||||
|
|
||||||
|
CONF_CYCLETIME = 'cycletime'
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'DINRelay'
|
||||||
|
DEFAULT_USERNAME = 'admin'
|
||||||
|
DEFAULT_PASSWORD = 'admin'
|
||||||
|
DEFAULT_TIMEOUT = 20
|
||||||
|
DEFAULT_CYCLETIME = 2
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||||
|
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||||
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT):
|
||||||
|
vol.All(vol.Coerce(int), vol.Range(min=1, max=600)),
|
||||||
|
vol.Optional(CONF_CYCLETIME, default=DEFAULT_CYCLETIME):
|
||||||
|
vol.All(vol.Coerce(int), vol.Range(min=1, max=600)),
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
"""Find and return DIN III Relay switch."""
|
||||||
|
import dlipower
|
||||||
|
|
||||||
|
host = config.get(CONF_HOST)
|
||||||
|
controllername = config.get(CONF_NAME)
|
||||||
|
user = config.get(CONF_USERNAME)
|
||||||
|
pswd = config.get(CONF_PASSWORD)
|
||||||
|
tout = config.get(CONF_TIMEOUT)
|
||||||
|
cycl = config.get(CONF_CYCLETIME)
|
||||||
|
|
||||||
|
power_switch = dlipower.PowerSwitch(
|
||||||
|
hostname=host, userid=user, password=pswd,
|
||||||
|
timeout=tout, cycletime=cycl
|
||||||
|
)
|
||||||
|
|
||||||
|
if not power_switch.verify():
|
||||||
|
_LOGGER.error('Could not connect to DIN III Relay')
|
||||||
|
return False
|
||||||
|
|
||||||
|
devices = []
|
||||||
|
parent_device = DINRelayDevice(power_switch)
|
||||||
|
|
||||||
|
devices.extend(
|
||||||
|
DINRelay(controllername, device.outlet_number, parent_device)
|
||||||
|
for device in power_switch
|
||||||
|
)
|
||||||
|
|
||||||
|
add_devices(devices)
|
||||||
|
|
||||||
|
|
||||||
|
class DINRelay(SwitchDevice):
|
||||||
|
"""Representation of a individual DIN III relay port."""
|
||||||
|
|
||||||
|
def __init__(self, name, outletnumber, parent_device):
|
||||||
|
"""Initialize the DIN III Relay switch."""
|
||||||
|
self._parent_device = parent_device
|
||||||
|
self.controllername = name
|
||||||
|
self.outletnumber = outletnumber
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the display name of this relay."""
|
||||||
|
return self._outletname
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if relay is on."""
|
||||||
|
return self._is_on
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""Polling is needed."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Instruct the relay to turn on."""
|
||||||
|
self._parent_device.turn_on(outlet=self.outletnumber)
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Instruct the relay to turn off."""
|
||||||
|
self._parent_device.turn_off(outlet=self.outletnumber)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Trigger update for all switches on the parent device."""
|
||||||
|
self._parent_device.update()
|
||||||
|
self._is_on = (
|
||||||
|
self._parent_device.statuslocal[self.outletnumber - 1][2] == 'ON'
|
||||||
|
)
|
||||||
|
self._outletname = "{}_{}".format(
|
||||||
|
self.controllername,
|
||||||
|
self._parent_device.statuslocal[self.outletnumber - 1][1]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DINRelayDevice(object):
|
||||||
|
"""Device representation for per device throttling."""
|
||||||
|
|
||||||
|
def __init__(self, device):
|
||||||
|
"""Initialize the DINRelay device."""
|
||||||
|
self._device = device
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def turn_on(self, **kwargs):
|
||||||
|
"""Instruct the relay to turn on."""
|
||||||
|
self._device.on(**kwargs)
|
||||||
|
|
||||||
|
def turn_off(self, **kwargs):
|
||||||
|
"""Instruct the relay to turn off."""
|
||||||
|
self._device.off(**kwargs)
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
def update(self):
|
||||||
|
"""Fetch new state data for this device."""
|
||||||
|
self.statuslocal = self._device.statuslist()
|
@ -11,7 +11,8 @@ import voluptuous as vol
|
|||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.components.pilight as pilight
|
import homeassistant.components.pilight as pilight
|
||||||
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
|
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SWITCHES, CONF_STATE)
|
from homeassistant.const import (CONF_NAME, CONF_ID, CONF_SWITCHES, CONF_STATE,
|
||||||
|
CONF_PROTOCOL)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -21,17 +22,20 @@ CONF_ON_CODE = 'on_code'
|
|||||||
CONF_ON_CODE_RECIEVE = 'on_code_receive'
|
CONF_ON_CODE_RECIEVE = 'on_code_receive'
|
||||||
CONF_SYSTEMCODE = 'systemcode'
|
CONF_SYSTEMCODE = 'systemcode'
|
||||||
CONF_UNIT = 'unit'
|
CONF_UNIT = 'unit'
|
||||||
|
CONF_UNITCODE = 'unitcode'
|
||||||
|
|
||||||
DEPENDENCIES = ['pilight']
|
DEPENDENCIES = ['pilight']
|
||||||
|
|
||||||
COMMAND_SCHEMA = pilight.RF_CODE_SCHEMA.extend({
|
COMMAND_SCHEMA = vol.Schema({
|
||||||
|
vol.Optional(CONF_PROTOCOL): cv.string,
|
||||||
vol.Optional('on'): cv.positive_int,
|
vol.Optional('on'): cv.positive_int,
|
||||||
vol.Optional('off'): cv.positive_int,
|
vol.Optional('off'): cv.positive_int,
|
||||||
vol.Optional(CONF_UNIT): cv.positive_int,
|
vol.Optional(CONF_UNIT): cv.positive_int,
|
||||||
|
vol.Optional(CONF_UNITCODE): cv.positive_int,
|
||||||
vol.Optional(CONF_ID): cv.positive_int,
|
vol.Optional(CONF_ID): cv.positive_int,
|
||||||
vol.Optional(CONF_STATE): cv.string,
|
vol.Optional(CONF_STATE): cv.string,
|
||||||
vol.Optional(CONF_SYSTEMCODE): cv.positive_int,
|
vol.Optional(CONF_SYSTEMCODE): cv.positive_int,
|
||||||
})
|
}, extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
SWITCHES_SCHEMA = vol.Schema({
|
SWITCHES_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_ON_CODE): COMMAND_SCHEMA,
|
vol.Required(CONF_ON_CODE): COMMAND_SCHEMA,
|
||||||
|
@ -4,23 +4,27 @@ Support for RESTful switches.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/switch.rest/
|
https://home-assistant.io/components/switch.rest/
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
import aiohttp
|
||||||
|
import async_timeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
|
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import (CONF_NAME, CONF_RESOURCE, CONF_TIMEOUT)
|
from homeassistant.const import (CONF_NAME, CONF_RESOURCE, CONF_TIMEOUT)
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.template import Template
|
from homeassistant.helpers.template import Template
|
||||||
|
|
||||||
CONF_BODY_OFF = 'body_off'
|
CONF_BODY_OFF = 'body_off'
|
||||||
CONF_BODY_ON = 'body_on'
|
CONF_BODY_ON = 'body_on'
|
||||||
|
CONF_IS_ON_TEMPLATE = 'is_on_template'
|
||||||
|
|
||||||
DEFAULT_BODY_OFF = Template('OFF')
|
DEFAULT_BODY_OFF = Template('OFF')
|
||||||
DEFAULT_BODY_ON = Template('ON')
|
DEFAULT_BODY_ON = Template('ON')
|
||||||
DEFAULT_NAME = 'REST Switch'
|
DEFAULT_NAME = 'REST Switch'
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
CONF_IS_ON_TEMPLATE = 'is_on_template'
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_RESOURCE): cv.url,
|
vol.Required(CONF_RESOURCE): cv.url,
|
||||||
@ -35,13 +39,15 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument,
|
# 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):
|
||||||
"""Set up the RESTful switch."""
|
"""Set up the RESTful switch."""
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
resource = config.get(CONF_RESOURCE)
|
resource = config.get(CONF_RESOURCE)
|
||||||
body_on = config.get(CONF_BODY_ON)
|
body_on = config.get(CONF_BODY_ON)
|
||||||
body_off = config.get(CONF_BODY_OFF)
|
body_off = config.get(CONF_BODY_OFF)
|
||||||
is_on_template = config.get(CONF_IS_ON_TEMPLATE)
|
is_on_template = config.get(CONF_IS_ON_TEMPLATE)
|
||||||
|
websession = async_get_clientsession(hass)
|
||||||
|
|
||||||
if is_on_template is not None:
|
if is_on_template is not None:
|
||||||
is_on_template.hass = hass
|
is_on_template.hass = hass
|
||||||
@ -51,19 +57,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
body_off.hass = hass
|
body_off.hass = hass
|
||||||
timeout = config.get(CONF_TIMEOUT)
|
timeout = config.get(CONF_TIMEOUT)
|
||||||
|
|
||||||
|
req = None
|
||||||
try:
|
try:
|
||||||
requests.get(resource, timeout=10)
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
except requests.exceptions.MissingSchema:
|
req = yield from websession.get(resource)
|
||||||
|
except (TypeError, ValueError):
|
||||||
_LOGGER.error("Missing resource or schema in configuration. "
|
_LOGGER.error("Missing resource or schema in configuration. "
|
||||||
"Add http:// or https:// to your URL")
|
"Add http:// or https:// to your URL")
|
||||||
return False
|
return False
|
||||||
except requests.exceptions.ConnectionError:
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||||
_LOGGER.error("No route to resource/endpoint: %s", resource)
|
_LOGGER.error("No route to resource/endpoint: %s", resource)
|
||||||
return False
|
return False
|
||||||
|
finally:
|
||||||
|
if req is not None:
|
||||||
|
yield from req.release()
|
||||||
|
|
||||||
add_devices(
|
yield from async_add_devices(
|
||||||
[RestSwitch(
|
[RestSwitch(hass, name, resource, body_on, body_off,
|
||||||
hass, name, resource, body_on, body_off, is_on_template, timeout)])
|
is_on_template, timeout)])
|
||||||
|
|
||||||
|
|
||||||
class RestSwitch(SwitchDevice):
|
class RestSwitch(SwitchDevice):
|
||||||
@ -73,7 +84,7 @@ class RestSwitch(SwitchDevice):
|
|||||||
is_on_template, timeout):
|
is_on_template, timeout):
|
||||||
"""Initialize the REST switch."""
|
"""Initialize the REST switch."""
|
||||||
self._state = None
|
self._state = None
|
||||||
self._hass = hass
|
self.hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
self._resource = resource
|
self._resource = resource
|
||||||
self._body_on = body_on
|
self._body_on = body_on
|
||||||
@ -91,46 +102,85 @@ class RestSwitch(SwitchDevice):
|
|||||||
"""Return true if device is on."""
|
"""Return true if device is on."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
@asyncio.coroutine
|
||||||
|
def async_turn_on(self, **kwargs):
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
body_on_t = self._body_on.render()
|
body_on_t = self._body_on.async_render()
|
||||||
request = requests.post(
|
websession = async_get_clientsession(self.hass)
|
||||||
self._resource, data=body_on_t, timeout=self._timeout)
|
|
||||||
if request.status_code == 200:
|
request = None
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
||||||
|
request = yield from websession.post(
|
||||||
|
self._resource, data=bytes(body_on_t, 'utf-8'))
|
||||||
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||||
|
_LOGGER.error("Error while turn on %s", self._resource)
|
||||||
|
return
|
||||||
|
finally:
|
||||||
|
if request is not None:
|
||||||
|
yield from request.release()
|
||||||
|
|
||||||
|
if request.status == 200:
|
||||||
self._state = True
|
self._state = True
|
||||||
else:
|
else:
|
||||||
_LOGGER.error("Can't turn on %s. Is resource/endpoint offline?",
|
_LOGGER.error("Can't turn on %s. Is resource/endpoint offline?",
|
||||||
self._resource)
|
self._resource)
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
@asyncio.coroutine
|
||||||
|
def async_turn_off(self, **kwargs):
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
body_off_t = self._body_off.render()
|
body_off_t = self._body_off.async_render()
|
||||||
request = requests.post(
|
websession = async_get_clientsession(self.hass)
|
||||||
self._resource, data=body_off_t, timeout=self._timeout)
|
|
||||||
if request.status_code == 200:
|
request = None
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
||||||
|
request = yield from websession.post(
|
||||||
|
self._resource, data=bytes(body_off_t, 'utf-8'))
|
||||||
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||||
|
_LOGGER.error("Error while turn off %s", self._resource)
|
||||||
|
return
|
||||||
|
finally:
|
||||||
|
if request is not None:
|
||||||
|
yield from request.release()
|
||||||
|
|
||||||
|
if request.status == 200:
|
||||||
self._state = False
|
self._state = False
|
||||||
else:
|
else:
|
||||||
_LOGGER.error("Can't turn off %s. Is resource/endpoint offline?",
|
_LOGGER.error("Can't turn off %s. Is resource/endpoint offline?",
|
||||||
self._resource)
|
self._resource)
|
||||||
|
|
||||||
def update(self):
|
@asyncio.coroutine
|
||||||
|
def async_update(self):
|
||||||
"""Get the latest data from REST API and update the state."""
|
"""Get the latest data from REST API and update the state."""
|
||||||
request = requests.get(self._resource, timeout=self._timeout)
|
websession = async_get_clientsession(self.hass)
|
||||||
|
|
||||||
|
request = None
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(self._timeout, loop=self.hass.loop):
|
||||||
|
request = yield from websession.get(self._resource)
|
||||||
|
text = yield from request.text()
|
||||||
|
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||||
|
_LOGGER.exception("Error while fetch data.")
|
||||||
|
return
|
||||||
|
finally:
|
||||||
|
if request is not None:
|
||||||
|
yield from request.release()
|
||||||
|
|
||||||
if self._is_on_template is not None:
|
if self._is_on_template is not None:
|
||||||
response = self._is_on_template.render_with_possible_json_value(
|
text = self._is_on_template.async_render_with_possible_json_value(
|
||||||
request.text, 'None')
|
text, 'None')
|
||||||
response = response.lower()
|
text = text.lower()
|
||||||
if response == 'true':
|
if text == 'true':
|
||||||
self._state = True
|
self._state = True
|
||||||
elif response == 'false':
|
elif text == 'false':
|
||||||
self._state = False
|
self._state = False
|
||||||
else:
|
else:
|
||||||
self._state = None
|
self._state = None
|
||||||
else:
|
else:
|
||||||
if request.text == self._body_on.template:
|
if text == self._body_on.template:
|
||||||
self._state = True
|
self._state = True
|
||||||
elif request.text == self._body_off.template:
|
elif text == self._body_off.template:
|
||||||
self._state = False
|
self._state = False
|
||||||
else:
|
else:
|
||||||
self._state = None
|
self._state = None
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user