Matthew Treinish 1f1115f631 Serialize mochad requests (#11029)
All mochad devices are sharing a single socket interface. When multiple
threads are issuing requests to the mochad daemon at the same time the
write read cycle might get crossed between the threads. This is normally
not an issue for 1-way X10 devices because as long as the request issued
successfully and data is read over the socket then we know as much as
mochad will tell us (since there is no ACK from the request for most
X10 devices). However, where it does matter is on the device __init__()
because we're relying on the mochad daemon's internal state to take an
educated guess at the device's state to intialize things with. When
there are multiple devices being initialized at the same time the wires
can get crossed between and the wrong device state may be read.

To address this potential issue this commit adds locking using a
semaphore around all pairs of send_cmd() and read_data() (which is what
pymochad.device.Device.get_status() does internally) calls to the mochad
controller to ensure we're only ever dealing with a single request at a
time.

Fixes mtreinish/pymochad#4
2017-12-08 09:18:52 -08:00

103 lines
3.1 KiB
Python

"""
Contains functionality to use a X10 dimmer over Mochad.
For more details about this platform, please refer to the documentation at
https://home.assistant.io/components/light.mochad/
"""
import logging
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, PLATFORM_SCHEMA)
from homeassistant.components import mochad
from homeassistant.const import (CONF_NAME, CONF_PLATFORM, CONF_DEVICES,
CONF_ADDRESS)
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['mochad']
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PLATFORM): mochad.DOMAIN,
CONF_DEVICES: [{
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_ADDRESS): cv.x10_address,
vol.Optional(mochad.CONF_COMM_TYPE): cv.string,
}]
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up X10 dimmers over a mochad controller."""
devs = config.get(CONF_DEVICES)
add_devices([MochadLight(
hass, mochad.CONTROLLER.ctrl, dev) for dev in devs])
return True
class MochadLight(Light):
"""Representation of a X10 dimmer over Mochad."""
def __init__(self, hass, ctrl, dev):
"""Initialize a Mochad Light Device."""
from pymochad import device
self._controller = ctrl
self._address = dev[CONF_ADDRESS]
self._name = dev.get(CONF_NAME,
'x10_light_dev_{}'.format(self._address))
self._comm_type = dev.get(mochad.CONF_COMM_TYPE, 'pl')
self.device = device.Device(ctrl, self._address,
comm_type=self._comm_type)
self._brightness = 0
self._state = self._get_device_status()
@property
def brightness(self):
"""Return the birghtness of this light between 0..255."""
return self._brightness
def _get_device_status(self):
"""Get the status of the light from mochad."""
with mochad.REQ_LOCK:
status = self.device.get_status().rstrip()
return status == 'on'
@property
def name(self):
"""Return the display name of this light."""
return self._name
@property
def is_on(self):
"""Return true if the light is on."""
return self._state
@property
def supported_features(self):
"""Return supported features."""
return SUPPORT_BRIGHTNESS
@property
def assumed_state(self):
"""X10 devices are normally 1-way so we have to assume the state."""
return True
def turn_on(self, **kwargs):
"""Send the command to turn the light on."""
self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
with mochad.REQ_LOCK:
self.device.send_cmd("xdim {}".format(self._brightness))
self._controller.read_data()
self._state = True
def turn_off(self, **kwargs):
"""Send the command to turn the light on."""
with mochad.REQ_LOCK:
self.device.send_cmd('off')
self._controller.read_data()
self._state = False