Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ADS component #10142

Merged
merged 42 commits into from
Dec 5, 2017
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7f74ab2
add ads hub, light and switch
Sep 15, 2017
4a09d7a
add binary sensor prototype
Sep 24, 2017
12a87c3
switch: use adsvar for connection
stlehmann Sep 25, 2017
3d69f45
fix some issues with binary sensor
stlehmann Sep 25, 2017
66a5226
fix binary sensor
stlehmann Sep 26, 2017
429d5b3
fix all platforms
stlehmann Sep 26, 2017
aefaf31
use latest pyads
Sep 29, 2017
6143c77
fixed error with multiple binary sensors
Oct 1, 2017
7aa7ce2
add sensor
Oct 1, 2017
1430213
Merge remote-tracking branch 'upstream/dev' into dev
Oct 1, 2017
79e4112
add ads sensor
Oct 2, 2017
af9f90f
clean up after shutdown
Oct 7, 2017
05a3f63
Merge remote-tracking branch 'upstream/dev' into dev
Oct 8, 2017
41bbcf0
ads component with platforms switch, binary_sensor, light, sensor
Oct 8, 2017
866cbb2
add ads service
Oct 23, 2017
14c0552
add default settings for use_notify and poll_interval
Oct 24, 2017
94bc9b3
fix too long line
Oct 25, 2017
10a7772
Fix style issues
Oct 27, 2017
b0999f4
no pydocstyle errors
Oct 27, 2017
0c3b28c
Send and receive native brightness data to ADS device to prevent issu…
carstenschroeder Nov 9, 2017
4631da4
Merge pull request #1 from carstenschroeder/patch-3
Nov 9, 2017
da54023
Enable non dimmable lights
carstenschroeder Nov 10, 2017
f6024a3
remove setting of self._state in switch
Nov 16, 2017
6de2c6b
Merge branch 'ads' of https://github.com/MrLeeh/home-assistant into ads
Nov 16, 2017
7da420f
remove polling
Nov 18, 2017
b3e1d2a
Revert "remove polling"
Nov 19, 2017
b66c2f6
add service schema, add links to documentation
Nov 19, 2017
ab4a6a9
fix naming, cleanup
Nov 19, 2017
6eb8926
re-remove polling
Nov 21, 2017
b655182
use async_added_to_hass for setup of callbacks
Nov 29, 2017
2589696
fix comment.
Nov 29, 2017
0b15de4
add callbacks for changed values
Dec 1, 2017
50fe60f
use async_add_job for creating device notifications
Dec 1, 2017
45f5c9f
set should_poll to False for all platforms
Dec 1, 2017
aaa2a8c
change should_poll to property
Dec 1, 2017
123f28a
add service description to services.yaml
Dec 3, 2017
5708793
Merge branch 'dev' into ads
Dec 3, 2017
126e90b
add for brigthness not being None
Dec 3, 2017
1972b9d
Merge branch 'patch-1' of git://github.com/carstenschroeder/home-assi…
Dec 3, 2017
c2dbc2d
put ads component in package
Dec 3, 2017
04980ec
Remove whitespace
Dec 3, 2017
5befcea
omit ads package
Dec 5, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ omit =
homeassistant/components/abode.py
homeassistant/components/*/abode.py

homeassistant/components/ads.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to update the path to the package here. Sorry that I missed that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Damn, I forgot about this. Should work now.

homeassistant/components/*/ads.py

homeassistant/components/alarmdecoder.py
homeassistant/components/*/alarmdecoder.py

Expand Down
207 changes: 207 additions & 0 deletions homeassistant/components/ads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
"""
ADS Component.

For more details about this component, please refer to the documentation.
https://home-assistant.io/components/ads/

"""
import threading
import struct
import logging
import ctypes
from collections import namedtuple
import voluptuous as vol
from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \
EVENT_HOMEASSISTANT_STOP
import homeassistant.helpers.config_validation as cv

REQUIREMENTS = ['pyads==2.2.6']

_LOGGER = logging.getLogger(__name__)

DATA_ADS = 'data_ads'

# Supported Types
ADSTYPE_INT = 'int'
ADSTYPE_UINT = 'uint'
ADSTYPE_BYTE = 'byte'
ADSTYPE_BOOL = 'bool'


ADS_PLATFORMS = ['switch', 'binary_sensor', 'light']
Copy link
Member

@MartinHjelmare MartinHjelmare Nov 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must have been from some artifact from the beginning where I thought I might need it. Removed it.

DOMAIN = 'ads'

# config variable names
CONF_ADSVAR = 'adsvar'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CONF_ADS_VAR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed it.

CONF_ADSTYPE = 'adstype'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CONF_ADS_TYPE

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed it.

CONF_ADS_USE_NOTIFY = 'use_notify'
CONF_ADS_POLL_INTERVAL = 'poll_interval'
CONF_ADS_FACTOR = 'factor'

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_IP_ADDRESS): cv.string,
vol.Optional(CONF_ADS_POLL_INTERVAL, default=1000): cv.positive_int,
vol.Optional(CONF_ADS_USE_NOTIFY, default=True): cv.boolean,
})
}, extra=vol.ALLOW_EXTRA)


def setup(hass, config):
import pyads
""" Set up the ADS component. """
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All docstrings needs to conform with PEP257 and need to be present.

_LOGGER.info('created ADS client')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's too verbose as it's already logged.

conf = config[DOMAIN]

# get ads connection parameters from config
net_id = conf.get(CONF_DEVICE)
ip_address = conf.get(CONF_IP_ADDRESS)
port = conf.get(CONF_PORT)
poll_interval = conf.get(CONF_ADS_POLL_INTERVAL)
use_notify = conf.get(CONF_ADS_USE_NOTIFY)

# create a new ads connection
client = pyads.Connection(net_id, port, ip_address)

# connect to ads client and try to connect
try:
ads = AdsHub(client, poll_interval=poll_interval,
use_notify=use_notify)
except pyads.pyads.ADSError as e:
_LOGGER.error('Could not connect to ADS host (netid={}, port={})'
.format(net_id, port))
return False

# add ads hub to hass data collection, listen to shutdown
hass.data[DATA_ADS] = ads
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, ads.shutdown)

def handle_write_data_by_name(call):
""" Write a value to the connected ADS device. """
adsvar = call.data.get('adsvar')
adstype = call.data.get('adstype')
value = call.data.get('value')

assert adstype in ads.ADS_TYPEMAP
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be validated by the voluptuous service schema.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually validated by voluptuous in the ADS sensor platform.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any service schema anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the service schema.


try:
ads.write_by_name(adsvar, value, ads.ADS_TYPEMAP[adstype])
except pyads.ADSError as e:
_LOGGER.error(e)

hass.services.register(DOMAIN, 'write_data_by_name',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a voluptuous validation service schema for the service.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

handle_write_data_by_name)

return True


# tuple to hold data needed for notification
NotificationItem = namedtuple(
'NotificationItem', 'hnotify huser name plc_datatype callback'
)


class AdsHub:
""" Representation of a PyADS connection. """

def __init__(self, ads_client, poll_interval, use_notify):
from pyads import PLCTYPE_BOOL, PLCTYPE_BYTE, PLCTYPE_INT, \
PLCTYPE_UINT, ADSError

self.ADS_TYPEMAP = {
ADSTYPE_BOOL: PLCTYPE_BOOL,
ADSTYPE_BYTE: PLCTYPE_BYTE,
ADSTYPE_INT: PLCTYPE_INT,
ADSTYPE_UINT: PLCTYPE_UINT,
}

self.PLCTYPE_BOOL = PLCTYPE_BOOL
self.PLCTYPE_BYTE = PLCTYPE_BYTE
self.PLCTYPE_INT = PLCTYPE_INT
self.PLCTYPE_UINT = PLCTYPE_UINT
self.ADSError = ADSError
self.poll_interval = poll_interval
self.use_notify = use_notify

self._client = ads_client
self._client.open()

# all ADS devices are registered here
self._devices = []
self._notification_items = {}
self._lock = threading.Lock()

def shutdown(self, *args, **kwargs):
_LOGGER.debug('Shutting down ADS')
for key, notification_item in self._notification_items.items():
self._client.del_device_notification(
notification_item.hnotify,
notification_item.huser
)
_LOGGER.debug('Deleting device notification {0}, {1}'
.format(notification_item.hnotify,
notification_item.huser))
self._client.close()

def register_device(self, device):
""" Register a new device. """
self._devices.append(device)

def write_by_name(self, name, value, plc_datatype):
with self._lock:
return self._client.write_by_name(name, value, plc_datatype)

def read_by_name(self, name, plc_datatype):
with self._lock:
return self._client.read_by_name(name, plc_datatype)

def add_device_notification(self, name, plc_datatype, callback):
from pyads import NotificationAttrib
""" Add a notification to the ADS devices. """
attr = NotificationAttrib(ctypes.sizeof(plc_datatype))

with self._lock:
hnotify, huser = self._client.add_device_notification(
name, attr, self._device_notification_callback
)
hnotify = int(hnotify)

_LOGGER.debug('Added Device Notification {0} for variable {1}'
.format(hnotify, name))

self._notification_items[hnotify] = NotificationItem(
hnotify, huser, name, plc_datatype, callback
)

def _device_notification_callback(self, addr, notification, huser):
from pyads import PLCTYPE_BOOL, PLCTYPE_INT, PLCTYPE_BYTE, PLCTYPE_UINT
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't these already available as class attributes after they were set on the class in setup?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I removed the import and refer to the class attributes.

contents = notification.contents

hnotify = int(contents.hNotification)
_LOGGER.debug('Received Notification {0}'.format(hnotify))
data = contents.data

try:
notification_item = self._notification_items[hnotify]
except KeyError:
_LOGGER.debug('Unknown Device Notification handle: {0}'
.format(hnotify))
return

# parse data to desired datatype
if notification_item.plc_datatype == PLCTYPE_BOOL:
value = bool(struct.unpack('<?', bytearray(data)[:1])[0])
elif notification_item.plc_datatype == PLCTYPE_INT:
value = struct.unpack('<h', bytearray(data)[:2])[0]
elif notification_item.plc_datatype == PLCTYPE_BYTE:
value = struct.unpack('<B', bytearray(data)[:1])[0]
elif notification_item.plc_datatype == PLCTYPE_UINT:
value = struct.unpack('<H', bytearray(data)[:2])[0]
else:
value = bytearray(data)
_LOGGER.warning('No callback available for this datatype.')

# execute callback
notification_item.callback(notification_item.name, value)
112 changes: 112 additions & 0 deletions homeassistant/components/binary_sensor/ads.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""
Support for ADS binary sensors.

"""
import logging
from datetime import timedelta

import voluptuous as vol

from homeassistant.components.binary_sensor import BinarySensorDevice, \
PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA
from homeassistant.components.ads import DATA_ADS, CONF_ADSVAR, \
CONF_ADS_USE_NOTIFY, CONF_ADS_POLL_INTERVAL
from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS
from homeassistant.helpers.event import async_track_time_interval
import homeassistant.helpers.config_validation as cv


_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = ['ads']
DEFAULT_NAME = 'ADS binary sensor'


PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADSVAR): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_ADS_USE_NOTIFY): cv.boolean,
vol.Optional(CONF_ADS_POLL_INTERVAL): cv.positive_int,
})


def setup_platform(hass, config, add_devices, discovery_info=None):
""" Set up the Binary Sensor platform for ADS. """
ads_hub = hass.data.get(DATA_ADS)
if not ads_hub:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be able to happen, since this platform depends on the ads component, which adds the DATA_ADS in hass.data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible that the ADS component can not be created if a communication with the device can not be established. In this case I need to check for a valid component. Does Homeassistant do this prior to calling the setup_platform function? In this case I would agree to remove this check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just found the answer in the documentation. So I will remove this check.

return False

adsvar = config.get(CONF_ADSVAR)
name = config.get(CONF_NAME)
device_class = config.get(CONF_DEVICE_CLASS)
use_notify = config.get(CONF_ADS_USE_NOTIFY, ads_hub.use_notify)
poll_interval = config.get(CONF_ADS_POLL_INTERVAL, ads_hub.poll_interval)

ads_sensor = AdsBinarySensor(ads_hub, name, adsvar, device_class,
use_notify, poll_interval)
add_devices([ads_sensor], True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no update method in the entity, so the second argument here won't do anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the second argument.


if use_notify:
ads_hub.add_device_notification(adsvar, ads_hub.PLCTYPE_BOOL,
ads_sensor.callback)
else:
dtime = timedelta(0, 0, poll_interval * 1000)
async_track_time_interval(hass, ads_sensor.poll, dtime)


class AdsBinarySensor(BinarySensorDevice):
""" Representation of ADS binary sensors. """

def __init__(self, ads_hub, name, adsvar, device_class, use_notify,
poll_interval):
self._name = name
self._state = False
self._device_class = device_class or 'moving'
self._ads_hub = ads_hub
self.adsvar = adsvar
self.use_notify = use_notify
self.poll_interval = poll_interval

# make first poll if notifications disabled
if not self.use_notify:
self.poll(None)

@property
def name(self):
""" Return the default name of the binary sensor. """
return self._name

@property
def device_class(self):
""" Return the device class. """
return self._device_class

@property
def is_on(self):
""" Return if the binary sensor is on. """
return self._state

def callback(self, name, value):
_LOGGER.debug('Variable "{0}" changed its value to "{1}"'
.format(name, value))
self._state = value
try:
self.schedule_update_ha_state()
except AttributeError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this. I suppose I need to call the schedule_update_ha_state() method in order to schedule an update of my platform state. If I skip this function the state of the platform will not be updated in the fronted. Clearly I am missing something here. Could you give me a hint.?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, don't remove the call to schedule_update_ha_state. My comment is at line 77. When you move the registration of the callback to the entity method async_added_to_hass, the attribute error can't happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

pass

def poll(self, now):
try:
self._state = self._ads_hub.read_by_name(
self.adsvar, self._ads_hub.PLCTYPE_BOOL
)
_LOGGER.debug('Polled value for bool variable {0}: {1}'
.format(self.adsvar, self._state))
except self._ads_hub.ADSError as e:
_LOGGER.error(e)

try:
self.schedule_update_ha_state()
except AttributeError:
pass
Loading