-
-
Notifications
You must be signed in to change notification settings - Fork 30.9k
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
Add ADS component #10142
Changes from 17 commits
7f74ab2
4a09d7a
12a87c3
3d69f45
66a5226
429d5b3
aefaf31
6143c77
7aa7ce2
1430213
79e4112
af9f90f
05a3f63
41bbcf0
866cbb2
14c0552
94bc9b3
10a7772
b0999f4
0c3b28c
4631da4
da54023
f6024a3
6de2c6b
7da420f
b3e1d2a
b66c2f6
ab4a6a9
6eb8926
b655182
2589696
0b15de4
50fe60f
45f5c9f
aaa2a8c
123f28a
5708793
126e90b
1972b9d
c2dbc2d
04980ec
5befcea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't seem to be used. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed it. |
||
CONF_ADSTYPE = 'adstype' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. """ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be validated by the voluptuous service schema. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is actually validated by voluptuous in the ADS sensor platform. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any service schema anywhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a voluptuous validation service schema for the service. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this. I suppose I need to call the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, don't remove the call to There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.