|
1 |
| -""" fds """ |
| 1 | +# MQTT Payload Processor for Home Assistant |
| 2 | +# Converts MQTT message payloads to Home Assistant events and callback scripts. |
| 3 | +# Useful for storing implementation specific codes in one location and using |
| 4 | +# HA specific abstractions throughout the configuration. |
| 5 | +# E.g. RF codes, Bluetooth ids, etc. |
| 6 | + |
| 7 | +# Documentation: https://github.com/danobot/mqtt_payload_processor |
| 8 | +# Version: v2.0.3 |
| 9 | + |
| 10 | +import homeassistant.loader as loader |
| 11 | +import logging |
| 12 | +import asyncio |
| 13 | +import json |
| 14 | +from datetime import datetime, timedelta, date, time |
| 15 | +from homeassistant.helpers.entity import Entity |
| 16 | +from custom_components.processor import ProcessorDevice |
| 17 | +from homeassistant.util import dt |
| 18 | +from homeassistant.core import HomeAssistant as hass, callback |
| 19 | +from homeassistant.components.script import ScriptEntity |
| 20 | +from homeassistant.loader import bind_hass |
| 21 | +import homeassistant.helpers.script as script |
| 22 | +from custom_components.processor.yaml_scheduler import Action, Scheduler, TimeSchedule, Mapping |
| 23 | +# from datetimerange import DateTimeRange |
| 24 | +VERSION = '2.2.0' |
| 25 | +DOMAIN = 'processor' |
| 26 | +DEPENDENCIES = ['mqtt'] |
| 27 | +# REQUIREMENTS = ['datetimerange'] |
| 28 | +CONF_TOPIC = 'topic' |
| 29 | +DEFAULT_TOPIC = '/rf/' |
| 30 | +ACTION_ON = 'on' |
| 31 | +ACTION_OFF = 'off' |
| 32 | +TYPE_WALLPANEL = 'panel' |
| 33 | +DEFAULT_ACTION = 'default' |
| 34 | +_LOGGER = logging.getLogger(__name__) |
| 35 | + |
| 36 | + |
| 37 | +def setup_platform(hass, config, add_entities, discovery_info=None): |
| 38 | + """Set up the processor platform.""" |
| 39 | + # # We only want this platform to be set up via discovery. |
| 40 | + _LOGGER.info("MQTT CODE PROCESSOR - platform setup") |
| 41 | + |
| 42 | + from homeassistant.components import mqtt |
| 43 | + |
| 44 | + entities = [] |
| 45 | + |
| 46 | + topic = config.get('topic', DEFAULT_TOPIC) |
| 47 | + _LOGGER.info("MQTT CODE PROCESSOR.PY - setup_platform topic: " + str(topic)) |
| 48 | + _LOGGER.info("MQTT CODE PROCESSOR.PY - setup_platform Platform Config: " + str(config)) |
| 49 | + _LOGGER.info("MQTT CODE PROCESSOR.PY - setup_platform discovery_info: " + str(discovery_info)) |
| 50 | + config_items = config['entities'] |
| 51 | + _LOGGER.info("Loading {} entities".format(str(len(config_items)))) |
| 52 | + for v in config_items: |
| 53 | + _LOGGER.info("Config Item: " + str(v)) |
| 54 | + v['globalCallbackScript'] = config.get('callback_script', None) |
| 55 | + v['globalEvent'] = config.get('event') |
| 56 | + |
| 57 | + # must be done here because self.hass inside `m` is None for some reason |
| 58 | + # (even though it inherits from `Entity`) |
| 59 | + entities.append(Device(v, mqtt, topic, hass)) |
| 60 | + |
| 61 | + |
| 62 | + add_entities(entities) |
| 63 | + |
| 64 | + return True |
| 65 | + |
| 66 | + |
| 67 | +class Device(ProcessorDevice): |
| 68 | + """ Represents a device such as wall panel, remote or button with 1 or more RF code buttons. Container class for Entities.""" |
| 69 | + def __init__(self, args, mqtt, topic, hass): |
| 70 | + self.log = logging.getLogger("{}.device.{}".format(__name__, args.get('name', 'unnamed'))) |
| 71 | + |
| 72 | + self._name = args.get('name', 'Unnamed Device') |
| 73 | + self._type = args.get('type', 'panel') |
| 74 | + self.may_update = False |
| 75 | + self._state = 'setting up' |
| 76 | + self._mapping_callbacks = [] |
| 77 | + self.attributes = {} |
| 78 | + self._mappings = [] |
| 79 | + for key, item in args.get('mappings').items(): # for each mapping |
| 80 | + item['globalLogbook'] = args.get('log', False) |
| 81 | + item['globalCallbackScript'] = args.get('globalCallbackScript', None) |
| 82 | + item['globalEvent'] = args.get('globalEvent') |
| 83 | + |
| 84 | + m = MqttButton(key, item, self) |
| 85 | + self._mappings.append(m) |
| 86 | + mqtt.subscribe(hass, topic, m.message_received) |
| 87 | + # self.update(**{key:m.last_triggered}) |
| 88 | + |
| 89 | + |
| 90 | + self._schedules = {} |
| 91 | + try: |
| 92 | + for key, item in args.get('schedules').items(): |
| 93 | + self._schedules[key] = TimeSchedule(key, item, self) |
| 94 | + # self._schedules[key] = ScheduleFactory.create(self, key, item, self) |
| 95 | + except AttributeError as a: |
| 96 | + self.log.debug("No schedules were defined.") |
| 97 | + |
| 98 | + if len(self._schedules) == 0: |
| 99 | + self.log.debug("No schedules defined.") |
| 100 | + else: |
| 101 | + self.log.debug("Some schedules defined.") |
| 102 | + |
| 103 | + @property |
| 104 | + def state(self): |
| 105 | + return self._state |
| 106 | + @property |
| 107 | + def name(self): |
| 108 | + """Return the state of the entity.""" |
| 109 | + return self._name |
| 110 | + # def add_observer(self, o): |
| 111 | + # self._mapping_callbacks.append(o) |
| 112 | + # def remove_observer(self, o): |
| 113 | + # self._mapping_callbacks.remoev(o) |
| 114 | + |
| 115 | + def handle_event(self, mapping): |
| 116 | + """ called by mapping when code is received, will call mapping with active schedule name """ |
| 117 | + # find active schedule |
| 118 | + schedules = self.get_active_schedules() |
| 119 | + mapping.run_actions(schedules) |
| 120 | + |
| 121 | + def get_active_schedules(self): |
| 122 | + """ Determine which schedules apply currently """ |
| 123 | + active = [] |
| 124 | + if self._schedules is not None: |
| 125 | + for name, schedule in self._schedules.items(): |
| 126 | + self.log.debug("Checking if {} is active".format(name)) |
| 127 | + if schedule.is_active(): |
| 128 | + active.append(schedule.name) |
| 129 | + |
| 130 | + if len(active) == 0: |
| 131 | + active.append(DEFAULT_ACTION) |
| 132 | + return active |
| 133 | + @property |
| 134 | + def state_attributes(self): |
| 135 | + """Return the state of the entity.""" |
| 136 | + return self.attributes.copy() |
| 137 | + def update(self, wait=False, **kwargs): |
| 138 | + """ Called from different methods to report a state attribute change """ |
| 139 | + # self.log.debug("Update called with {}".format(str(kwargs))) |
| 140 | + for k,v in kwargs.items(): |
| 141 | + if v is not None: |
| 142 | + self.set_attr(k,v) |
| 143 | + |
| 144 | + if wait == False: |
| 145 | + self.do_update() |
| 146 | + def reset_state(self): |
| 147 | + """ Reset state attributes by removing any state specific attributes when returning to idle state """ |
| 148 | + self.model.log.debug("Resetting state") |
| 149 | + att = {} |
| 150 | + |
| 151 | + PERSISTED_STATE_ATTRIBUTES = [ |
| 152 | + 'last_triggered_by', |
| 153 | + 'last_triggered_at', |
| 154 | + 'state_entities', |
| 155 | + 'control_entities', |
| 156 | + 'sensor_entities', |
| 157 | + 'override_entities', |
| 158 | + 'delay', |
| 159 | + 'sensor_type', |
| 160 | + 'mode' |
| 161 | + ] |
| 162 | + for k,v in self.attributes.items(): |
| 163 | + if k in PERSISTED_STATE_ATTRIBUTES: |
| 164 | + att[k] = v |
| 165 | + |
| 166 | + self.attributes = att |
| 167 | + self.do_update() |
| 168 | + |
| 169 | + def do_update(self, wait=False,**kwargs): |
| 170 | + """ Schedules an entity state update with HASS """ |
| 171 | + # _LOGGER.debug("Scheduled update with HASS") |
| 172 | + if self.may_update: |
| 173 | + self.async_schedule_update_ha_state(True) |
| 174 | + |
| 175 | + def set_attr(self, k, v): |
| 176 | + # _LOGGER.debug("Setting state attribute {} to {}".format(k, v)) |
| 177 | + if k == 'delay': |
| 178 | + v = str(v) + 's' |
| 179 | + self.attributes[k] = v |
| 180 | + # self.do_update() |
| 181 | + # _LOGGER.debug("State attributes: " + str(self.attributes)) |
| 182 | + # HA Callbacks |
| 183 | + async def async_added_to_hass(self): |
| 184 | + """Register update dispatcher.""" |
| 185 | + self.may_update = True |
| 186 | + self._state = "ready" |
| 187 | + self.do_update() |
| 188 | + |
| 189 | + |
| 190 | +class MqttButton(Mapping): |
| 191 | + """ Represents a single button """ |
| 192 | + |
| 193 | + type = None |
| 194 | + |
| 195 | + def __init__(self, name, config, device): |
| 196 | + super(MqttButton, self).__init__(name, config, device) |
| 197 | + self.last_payload = None |
| 198 | + self.last_triggered = 'never' |
| 199 | + self.last_action = 'none' |
| 200 | + self.payloads_on = [] |
| 201 | + self.payloads_off = [] |
| 202 | + # self.alert = Alert( |
| 203 | + # self.device.hass, |
| 204 | + # object_id, |
| 205 | + # name, |
| 206 | + # watched_entity_id, |
| 207 | + # alert_state, |
| 208 | + # repeat, |
| 209 | + # skip_first, |
| 210 | + # message_template, |
| 211 | + # done_message_template, |
| 212 | + # notifiers, |
| 213 | + # can_ack, |
| 214 | + # title_template, |
| 215 | + # data, |
| 216 | + # ) |
| 217 | + self.name = name |
| 218 | + # self.log = logging.getLogger(__name__ + '.' + self.name) |
| 219 | + self.log.debug("Init Config: " +str(config)) |
| 220 | + self.log.debug("Payloads: " +str(self.payloads_on)) |
| 221 | + if 'payload' in config: |
| 222 | + self.payloads_on.append(config.get('payload')) |
| 223 | + if 'payloads' in config: |
| 224 | + self.payloads_on.extend(config.get('payloads')) |
| 225 | + if 'payloads_on' in config: |
| 226 | + self.payloads_on.extend(config.get('payloads_on')) |
| 227 | + |
| 228 | + if 'payload_off' in config: |
| 229 | + self.payloads_off.append(config.get('payload_off')) |
| 230 | + if 'payloads_off' in config: |
| 231 | + self.payloads_off.extend(config.get('payloads_off')) |
| 232 | + |
| 233 | + self.log.debug("Payloads ON: " + str(self.payloads_on)) |
| 234 | + self.log.debug("Payloads OFF: " + str(self.payloads_off)) |
| 235 | + self.type = config.get('type', None) |
| 236 | + self.event = config.get('event', False) |
| 237 | + self.callback = config.get('callback', False) |
| 238 | + self.callback_script = config.get('callback_script', False) |
| 239 | + self.globalCallbackScript = config.get('globalCallbackScript', False) |
| 240 | + self.log_events = config.get('globalLogbook', False) or config.get('log', False) |
| 241 | + self.globalEvent = config.get('globalEvent', False) |
| 242 | + |
| 243 | + self._always_active = False |
| 244 | + self.device.update(**{self.name:self.last_triggered}) |
| 245 | + |
| 246 | + |
| 247 | + |
| 248 | + # @property |
| 249 | + # def device_state_attributes(self): |
| 250 | + # return { |
| 251 | + # 'last_triggered': self.last_triggered, |
| 252 | + # 'last_payload': self.last_payload, |
| 253 | + # 'last_action': self.last_action, |
| 254 | + # 'payloads_on': self.payloads_on, |
| 255 | + # 'payloads_off': self.payloads_off, |
| 256 | + # 'callback': self.callback, |
| 257 | + # 'callback_script': self.callback_script, |
| 258 | + # 'global_callback_script': self.globalCallbackScript, |
| 259 | + # 'event': self.event, |
| 260 | + # 'global_event': self.globalEvent, |
| 261 | + # 'type': self.type |
| 262 | + # } |
| 263 | + |
| 264 | + def process(self, payload): |
| 265 | + j = json.loads(payload) |
| 266 | + # self.log.debug("Called process on %s %s" % (str(j), str(dir(j)))) |
| 267 | + # single payload defined |
| 268 | + # for k in j.keys(): |
| 269 | + # self.log.debug("%s %s" % (k, j[k])) |
| 270 | + value = j["value"] |
| 271 | + # self.log.debug("Is %s a match for %s?" % (value, self.name)) |
| 272 | + for p in self.payloads_on: |
| 273 | + if int(p) == value: |
| 274 | + self.log.info("Processing %s on code" % (p)) |
| 275 | + self.handleRFCode(ACTION_ON) |
| 276 | + self.update_state(payload, ACTION_ON) |
| 277 | + break |
| 278 | + |
| 279 | + for p in self.payloads_off: |
| 280 | + if int(p) == value: |
| 281 | + self.log.info("Processing %s off code" % (p)) |
| 282 | + self.handleRFCode(ACTION_OFF) |
| 283 | + self.update_state(payload, ACTION_OFF) |
| 284 | + break |
| 285 | + |
| 286 | + def message_received(self, message): |
| 287 | + """Handle new MQTT messages.""" |
| 288 | + |
| 289 | + self.log.debug("Message received: " + str(message)) |
| 290 | + |
| 291 | + self.process(message.payload) |
| 292 | + |
| 293 | + |
| 294 | + def update_state(self, payload, action): |
| 295 | + self.last_payload = payload |
| 296 | + self.last_action = action |
| 297 | + self.last_triggered = dt.now() |
| 298 | + self.log.debug("name is " + self.name) |
| 299 | + self.device.update(**{self.name:self.last_triggered}) |
| 300 | + if self.log_events: |
| 301 | + log_data= { |
| 302 | + 'name': str( self.name) , |
| 303 | + 'message': " was triggered by RF code " + str(payload), |
| 304 | + 'entity_id': self.device.entity_id, |
| 305 | + 'domain': 'processor' |
| 306 | + } |
| 307 | + # if self.type == 'button': |
| 308 | + # log_data['message'] = 'was pressed' |
| 309 | + |
| 310 | + # if self.type == 'motion': |
| 311 | + # log_data['message'] = 'was activated' |
| 312 | + |
| 313 | + self.device.hass.services.call('logbook', 'log', log_data) |
| 314 | + # self.async_schedule_update_ha_state(True) |
| 315 | + |
| 316 | + |
| 317 | + |
| 318 | + def handleRFCode(self, action): |
| 319 | + # self.hass.states.set(DOMAIN + '.last_triggered_by', self.name) |
| 320 | + # hass.states.set('rf_processor.last_triggered_time', time.localtime(time.time())) |
| 321 | + # self.log.debug("event: " + str(self.event)) |
| 322 | + # self.log.debug("globalEvent: " + str(self.globalEvent)) |
| 323 | + self.device.handle_event(self) |
| 324 | + if self.event or self.globalEvent: |
| 325 | + self.log.debug("Sending event.") |
| 326 | + self.device.hass.bus.fire(self.name, { |
| 327 | + 'state': action |
| 328 | + }) |
| 329 | + |
| 330 | + |
| 331 | + |
| 332 | + if self.globalCallbackScript is not None and self.callback: |
| 333 | + self.log.info("Running global callback script: " + self.globalCallbackScript) |
| 334 | + self.device.hass.services.call('script', 'turn_on', {'entity_id': self.globalCallbackScript}) |
| 335 | + |
| 336 | + if self.callback_script: |
| 337 | + device, script = self.callback_script.split('.') |
| 338 | + self.log.info("Running device callback script: " + script) |
| 339 | + self.device.hass.services.call('script', 'turn_on', {'entity_id': self.callback_script}) |
| 340 | + |
| 341 | + @property |
| 342 | + def state(self): |
| 343 | + """Return the state of the entity.""" |
| 344 | + return None |
| 345 | + |
| 346 | + @property |
| 347 | + def state_attributes(self): |
| 348 | + """Return the state of the entity.""" |
| 349 | + |
| 350 | + time = str(datetime.datetime.now()) |
| 351 | + |
| 352 | + state = { |
| 353 | + 'last_triggered': time, |
| 354 | + 'payload': self.last_payload |
| 355 | + } |
| 356 | + |
| 357 | + if self.type: |
| 358 | + state['type'] = self.type |
| 359 | + |
| 360 | + if self.callback_script: |
| 361 | + state['callback_script'] = self.callback_script |
| 362 | + |
| 363 | + if self.callback: |
| 364 | + state['callback'] = self.callback |
| 365 | + |
| 366 | + if self.globalCallbackScript is not None: |
| 367 | + state['global_callback'] = self.globalCallbackScript |
| 368 | + if self.globalEvent: |
| 369 | + state['globalEvent'] = True |
| 370 | + |
| 371 | + return state |
| 372 | + |
| 373 | + |
0 commit comments