core/homeassistant/components/rfxtrx/__init__.py

425 lines
12 KiB
Python
Raw Normal View History

"""Support for RFXtrx devices."""
2016-04-23 17:55:05 +00:00
from collections import OrderedDict
2018-05-14 11:05:52 +00:00
import logging
import voluptuous as vol
2016-02-19 05:27:50 +00:00
from homeassistant.const import (
2019-07-31 19:25:30 +00:00
ATTR_ENTITY_ID,
ATTR_NAME,
ATTR_STATE,
CONF_DEVICE,
CONF_DEVICES,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
TEMP_CELSIUS,
POWER_WATT,
)
2018-05-14 11:05:52 +00:00
import homeassistant.helpers.config_validation as cv
2016-04-06 17:34:51 +00:00
from homeassistant.helpers.entity import Entity
2018-05-14 11:05:52 +00:00
from homeassistant.util import slugify
2019-07-31 19:25:30 +00:00
DOMAIN = "rfxtrx"
2016-04-24 09:48:01 +00:00
DEFAULT_SIGNAL_REPETITIONS = 1
2019-07-31 19:25:30 +00:00
ATTR_AUTOMATIC_ADD = "automatic_add"
ATTR_DEVICE = "device"
ATTR_DEBUG = "debug"
ATTR_FIRE_EVENT = "fire_event"
ATTR_DATA_TYPE = "data_type"
ATTR_DUMMY = "dummy"
CONF_DATA_BITS = "data_bits"
CONF_AUTOMATIC_ADD = "automatic_add"
CONF_DATA_TYPE = "data_type"
CONF_SIGNAL_REPETITIONS = "signal_repetitions"
CONF_FIRE_EVENT = "fire_event"
CONF_DUMMY = "dummy"
CONF_DEBUG = "debug"
CONF_OFF_DELAY = "off_delay"
EVENT_BUTTON_PRESSED = "button_pressed"
DATA_TYPES = OrderedDict(
[
("Temperature", TEMP_CELSIUS),
("Temperature2", TEMP_CELSIUS),
("Humidity", "%"),
("Barometer", ""),
("Wind direction", ""),
("Rain rate", ""),
("Energy usage", POWER_WATT),
("Total usage", POWER_WATT),
("Sound", ""),
("Sensor Status", ""),
("Counter value", ""),
("UV", "uv"),
("Humidity status", ""),
("Forecast", ""),
("Forecast numeric", ""),
("Rain total", ""),
("Wind average speed", ""),
("Wind gust", ""),
("Chill", ""),
("Total usage", ""),
("Count", ""),
("Current Ch. 1", ""),
("Current Ch. 2", ""),
("Current Ch. 3", ""),
("Energy usage", ""),
("Voltage", ""),
("Current", ""),
("Battery numeric", ""),
("Rssi numeric", ""),
]
)
2016-04-24 09:48:01 +00:00
2015-09-27 09:13:49 +00:00
RECEIVED_EVT_SUBSCRIBERS = []
RFX_DEVICES = {}
_LOGGER = logging.getLogger(__name__)
2019-07-31 19:25:30 +00:00
DATA_RFXOBJECT = "rfxobject"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_DEVICE): cv.string,
vol.Optional(CONF_DEBUG, default=False): cv.boolean,
vol.Optional(CONF_DUMMY, default=False): cv.boolean,
}
)
},
extra=vol.ALLOW_EXTRA,
)
2015-09-27 09:13:49 +00:00
def setup(hass, config):
"""Set up the RFXtrx component."""
2015-09-27 09:13:49 +00:00
# Declare the Handle event
def handle_receive(event):
"""Handle received messages from RFXtrx gateway."""
2015-10-03 09:26:18 +00:00
# Log RFXCOM event
if not event.device.id_string:
return
2019-07-31 19:25:30 +00:00
_LOGGER.debug(
"Receive RFXCOM event from "
"(Device_id: %s Class: %s Sub: %s, Pkt_id: %s)",
slugify(event.device.id_string.lower()),
event.device.__class__.__name__,
event.device.subtype,
"".join("{0:02x}".format(x) for x in event.data),
)
2015-10-03 09:26:18 +00:00
2016-03-07 17:49:31 +00:00
# Callback to HA registered components.
2015-09-27 09:13:49 +00:00
for subscriber in RECEIVED_EVT_SUBSCRIBERS:
subscriber(event)
2016-03-07 17:49:31 +00:00
# Try to load the RFXtrx module.
2016-01-29 05:37:08 +00:00
import RFXtrx as rfxtrxmod
2015-09-27 09:13:49 +00:00
device = config[DOMAIN][ATTR_DEVICE]
debug = config[DOMAIN][ATTR_DEBUG]
dummy_connection = config[DOMAIN][ATTR_DUMMY]
if dummy_connection:
rfx_object = rfxtrxmod.Connect(
2019-07-31 19:25:30 +00:00
device, None, debug=debug, transport_protocol=rfxtrxmod.DummyTransport2
)
else:
rfx_object = rfxtrxmod.Connect(device, None, debug=debug)
def _start_rfxtrx(event):
rfx_object.event_callback = handle_receive
2019-07-31 19:25:30 +00:00
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_rfxtrx)
2015-09-27 09:13:49 +00:00
def _shutdown_rfxtrx(event):
"""Close connection with RFXtrx."""
rfx_object.close_connection()
2019-07-31 19:25:30 +00:00
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_rfxtrx)
hass.data[DATA_RFXOBJECT] = rfx_object
return True
2015-10-06 06:44:15 +00:00
def get_rfx_object(packetid):
2016-03-07 17:49:31 +00:00
"""Return the RFXObject with the packetid."""
2016-01-29 05:37:08 +00:00
import RFXtrx as rfxtrxmod
2015-10-03 09:26:18 +00:00
try:
binarypacket = bytearray.fromhex(packetid)
except ValueError:
return None
pkt = rfxtrxmod.lowlevel.parse(binarypacket)
2016-04-24 09:48:01 +00:00
if pkt is None:
return None
if isinstance(pkt, rfxtrxmod.lowlevel.SensorPacket):
obj = rfxtrxmod.SensorEvent(pkt)
elif isinstance(pkt, rfxtrxmod.lowlevel.Status):
obj = rfxtrxmod.StatusEvent(pkt)
else:
obj = rfxtrxmod.ControlEvent(pkt)
return obj
2016-04-06 17:34:51 +00:00
def get_pt2262_deviceid(device_id, nb_data_bits):
"""Extract and return the address bits from a Lighting4/PT2262 packet."""
if nb_data_bits is None:
return
import binascii
2019-07-31 19:25:30 +00:00
try:
data = bytearray.fromhex(device_id)
except ValueError:
return None
mask = 0xFF & ~((1 << nb_data_bits) - 1)
2019-07-31 19:25:30 +00:00
data[len(data) - 1] &= mask
return binascii.hexlify(data)
def get_pt2262_cmd(device_id, data_bits):
"""Extract and return the data bits from a Lighting4/PT2262 packet."""
try:
data = bytearray.fromhex(device_id)
except ValueError:
return None
mask = 0xFF & ((1 << data_bits) - 1)
return hex(data[-1] & mask)
def get_pt2262_device(device_id):
"""Look for the device which id matches the given device_id parameter."""
for device in RFX_DEVICES.values():
2019-07-31 19:25:30 +00:00
if (
hasattr(device, "is_lighting4")
and device.masked_id is not None
and device.masked_id == get_pt2262_deviceid(device_id, device.data_bits)
):
_LOGGER.debug(
"rfxtrx: found matching device %s for %s", device_id, device.masked_id
)
return device
return None
def find_possible_pt2262_device(device_id):
"""Look for the device which id matches the given device_id parameter."""
for dev_id, device in RFX_DEVICES.items():
2019-07-31 19:25:30 +00:00
if hasattr(device, "is_lighting4") and len(dev_id) == len(device_id):
size = None
for i, (char1, char2) in enumerate(zip(dev_id, device_id)):
if char1 != char2:
break
size = i
if size is not None:
size = len(dev_id) - size - 1
2019-07-31 19:25:30 +00:00
_LOGGER.info(
"rfxtrx: found possible device %s for %s "
"with the following configuration:\n"
"data_bits=%d\n"
"command_on=0x%s\n"
"command_off=0x%s\n",
device_id,
dev_id,
size * 4,
dev_id[-size:],
device_id[-size:],
)
return device
return None
def get_devices_from_config(config, device):
2016-04-06 17:34:51 +00:00
"""Read rfxtrx configuration."""
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
2016-04-06 17:34:51 +00:00
devices = []
2016-04-23 17:55:05 +00:00
for packet_id, entity_info in config[CONF_DEVICES].items():
event = get_rfx_object(packet_id)
if event is None:
_LOGGER.error("Invalid device: %s", packet_id)
continue
2016-04-23 17:55:05 +00:00
device_id = slugify(event.device.id_string.lower())
2016-04-06 17:34:51 +00:00
if device_id in RFX_DEVICES:
continue
_LOGGER.debug("Add %s rfxtrx", entity_info[ATTR_NAME])
2016-04-06 17:34:51 +00:00
# Check if i must fire event
fire_event = entity_info[ATTR_FIRE_EVENT]
datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: fire_event}
2016-04-06 17:34:51 +00:00
2019-07-31 19:25:30 +00:00
new_device = device(entity_info[ATTR_NAME], event, datas, signal_repetitions)
2016-04-06 17:34:51 +00:00
RFX_DEVICES[device_id] = new_device
devices.append(new_device)
return devices
def get_new_device(event, config, device):
2016-04-06 17:34:51 +00:00
"""Add entity if not exist and the automatic_add is True."""
device_id = slugify(event.device.id_string.lower())
2016-04-23 17:55:05 +00:00
if device_id in RFX_DEVICES:
return
2016-04-24 09:48:01 +00:00
if not config[ATTR_AUTOMATIC_ADD]:
2016-04-23 17:55:05 +00:00
return
2016-08-12 18:46:54 +00:00
pkt_id = "".join("{0:02x}".format(x) for x in event.data)
_LOGGER.debug(
2016-08-12 18:46:54 +00:00
"Automatic add %s rfxtrx device (Class: %s Sub: %s Packet_id: %s)",
2016-04-23 17:55:05 +00:00
device_id,
event.device.__class__.__name__,
2016-08-12 18:46:54 +00:00
event.device.subtype,
2019-07-31 19:25:30 +00:00
pkt_id,
2016-04-23 17:55:05 +00:00
)
datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: False}
2016-04-23 17:55:05 +00:00
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
2019-07-31 19:25:30 +00:00
new_device = device(pkt_id, event, datas, signal_repetitions)
2016-04-23 17:55:05 +00:00
RFX_DEVICES[device_id] = new_device
return new_device
2016-04-06 17:34:51 +00:00
def apply_received_command(event):
"""Apply command from rfxtrx."""
device_id = slugify(event.device.id_string.lower())
# Check if entity exists or previously added automatically
2016-04-23 17:55:05 +00:00
if device_id not in RFX_DEVICES:
return
_LOGGER.debug(
2019-07-31 19:25:30 +00:00
"Device_id: %s device_update. Command: %s", device_id, event.values["Command"]
2016-04-23 17:55:05 +00:00
)
2019-07-31 19:25:30 +00:00
if event.values["Command"] == "On" or event.values["Command"] == "Off":
2016-04-23 17:55:05 +00:00
# Update the rfxtrx device state
2019-07-31 19:25:30 +00:00
is_on = event.values["Command"] == "On"
2016-04-23 17:55:05 +00:00
RFX_DEVICES[device_id].update_state(is_on)
2019-07-31 19:25:30 +00:00
elif (
hasattr(RFX_DEVICES[device_id], "brightness")
and event.values["Command"] == "Set level"
):
_brightness = event.values["Dim level"] * 255 // 100
2016-04-23 17:55:05 +00:00
# Update the rfxtrx device state
is_on = _brightness > 0
RFX_DEVICES[device_id].update_state(is_on, _brightness)
# Fire event
if RFX_DEVICES[device_id].should_fire_event:
RFX_DEVICES[device_id].hass.bus.fire(
2019-07-31 19:25:30 +00:00
EVENT_BUTTON_PRESSED,
{
ATTR_ENTITY_ID: RFX_DEVICES[device_id].entity_id,
ATTR_STATE: event.values["Command"].lower(),
},
2016-04-06 17:34:51 +00:00
)
_LOGGER.debug(
2016-12-19 08:21:40 +00:00
"Rfxtrx fired event: (event_type: %s, %s: %s, %s: %s)",
EVENT_BUTTON_PRESSED,
ATTR_ENTITY_ID,
RFX_DEVICES[device_id].entity_id,
ATTR_STATE,
2019-07-31 19:25:30 +00:00
event.values["Command"].lower(),
2016-12-19 08:21:40 +00:00
)
2016-04-06 17:34:51 +00:00
class RfxtrxDevice(Entity):
"""Represents a Rfxtrx device.
2016-04-24 09:48:01 +00:00
Contains the common logic for Rfxtrx lights and switches.
2016-04-06 17:34:51 +00:00
"""
def __init__(self, name, event, datas, signal_repetitions):
"""Initialize the device."""
2016-04-23 17:55:05 +00:00
self.signal_repetitions = signal_repetitions
2016-04-06 17:34:51 +00:00
self._name = name
self._event = event
self._state = datas[ATTR_STATE]
self._should_fire_event = datas[ATTR_FIRE_EVENT]
2016-04-06 17:34:51 +00:00
self._brightness = 0
self.added_to_hass = False
async def async_added_to_hass(self):
"""Subscribe RFXtrx events."""
self.added_to_hass = True
2016-04-06 17:34:51 +00:00
@property
def should_poll(self):
"""No polling needed for a RFXtrx switch."""
return False
@property
def name(self):
"""Return the name of the device if any."""
return self._name
@property
def should_fire_event(self):
"""Return is the device must fire event."""
return self._should_fire_event
@property
def is_on(self):
2016-04-19 14:13:58 +00:00
"""Return true if device is on."""
2016-04-06 17:34:51 +00:00
return self._state
@property
def assumed_state(self):
"""Return true if unable to access real state of entity."""
return True
def turn_off(self, **kwargs):
2016-04-19 14:13:58 +00:00
"""Turn the device off."""
2016-04-06 17:34:51 +00:00
self._send_command("turn_off")
2016-04-23 17:55:05 +00:00
def update_state(self, state, brightness=0):
"""Update det state of the device."""
self._state = state
self._brightness = brightness
if self.added_to_hass:
self.schedule_update_ha_state()
2016-04-23 17:55:05 +00:00
2016-04-06 17:34:51 +00:00
def _send_command(self, command, brightness=0):
if not self._event:
return
rfx_object = self.hass.data[DATA_RFXOBJECT]
2016-04-06 17:34:51 +00:00
if command == "turn_on":
for _ in range(self.signal_repetitions):
self._event.device.send_on(rfx_object.transport)
2016-04-06 17:34:51 +00:00
self._state = True
elif command == "dim":
for _ in range(self.signal_repetitions):
self._event.device.send_dim(rfx_object.transport, brightness)
2016-04-06 17:34:51 +00:00
self._state = True
2019-07-31 19:25:30 +00:00
elif command == "turn_off":
2016-04-06 17:34:51 +00:00
for _ in range(self.signal_repetitions):
self._event.device.send_off(rfx_object.transport)
2016-04-06 17:34:51 +00:00
self._state = False
self._brightness = 0
elif command == "roll_up":
for _ in range(self.signal_repetitions):
self._event.device.send_open(rfx_object.transport)
elif command == "roll_down":
for _ in range(self.signal_repetitions):
self._event.device.send_close(rfx_object.transport)
elif command == "stop_roll":
for _ in range(self.signal_repetitions):
self._event.device.send_stop(rfx_object.transport)
if self.added_to_hass:
self.schedule_update_ha_state()