Skip to content

Commit 74d167d

Browse files
author
Daniel Mason
committed
Merge branch 'develop' into master
2 parents 4876e55 + d77dd72 commit 74d167d

File tree

8 files changed

+382
-442
lines changed

8 files changed

+382
-442
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.vscode/arduino.json
22
.vscode/c_cpp_properties.json
33
*.pyc
4+
custom_components_old

custom_components/mqtt_code/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
1313
"""Set up climate entities."""
14-
_LOGGER.info("mqtt_code/__init__.py - async_setup")
14+
_LOGGER.info("mqtt_code/__init__.py - async_setup" + str(config))
1515
component = hass.data[DOMAIN] = EntityComponent(
1616
_LOGGER, DOMAIN, hass)
1717
await component.async_setup(config)

custom_components/mqtt_code/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
"codeowners": ["@danobot"],
88
"requirements": [],
99
"iot_class": "local_polling",
10-
"version": "2.1.0"
10+
"version": "2.2.0"
1111
}
+373-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,373 @@
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

Comments
 (0)