Smarthome Functionen
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. from base import mqtt_base
  5. from base import videv_base
  6. import json
  7. def is_json(data):
  8. try:
  9. json.loads(data)
  10. except json.decoder.JSONDecodeError:
  11. return False
  12. else:
  13. return True
  14. class base(mqtt_base):
  15. TX_TOPIC = "set"
  16. TX_VALUE = 0
  17. TX_DICT = 1
  18. TX_TYPE = -1
  19. TX_FILTER_DATA_KEYS = []
  20. #
  21. RX_KEYS = []
  22. RX_IGNORE_TOPICS = []
  23. RX_IGNORE_KEYS = []
  24. RX_FILTER_DATA_KEYS = []
  25. #
  26. CFG_DATA = {}
  27. def __init__(self, mqtt_client, topic):
  28. super().__init__(mqtt_client, topic, default_values=dict.fromkeys(self.RX_KEYS))
  29. # data storage
  30. self.__cfg_by_mid__ = None
  31. # initialisations
  32. mqtt_client.add_callback(topic=self.topic, callback=self.receive_callback)
  33. mqtt_client.add_callback(topic=self.topic+"/#", callback=self.receive_callback)
  34. #
  35. self.add_callback(None, None, self.__state_logging__, on_change_only=True)
  36. def __cfg_callback__(self, key, value, mid):
  37. if self.CFG_DATA.get(key) != value and self.__cfg_by_mid__ != mid and mid is not None:
  38. self.__cfg_by_mid__ = mid
  39. self.logger.warning("Differing configuration identified: Sending default configuration to defice: %s", repr(self.CFG_DATA))
  40. if self.TX_TYPE == self.TX_DICT:
  41. self.mqtt_client.send(self.topic + '/' + self.TX_TOPIC, json.dumps(self.CFG_DATA))
  42. else:
  43. for key in self.CFG_DATA:
  44. self.send_command(key, self.CFG_DATA.get(key))
  45. def set(self, key, data, mid=None, block_callback=[]):
  46. if key in self.CFG_DATA:
  47. self.__cfg_callback__(key, data, mid)
  48. if key in self.RX_IGNORE_KEYS:
  49. pass # ignore these keys
  50. elif key in self.RX_KEYS:
  51. return super().set(key, data, block_callback)
  52. else:
  53. self.logger.warning("Unexpected key %s", key)
  54. def receive_callback(self, client, userdata, message):
  55. if message.topic != self.topic + '/' + videv_base.KEY_INFO:
  56. content_key = message.topic[len(self.topic) + 1:]
  57. if content_key not in self.RX_IGNORE_TOPICS and (not message.topic.endswith(self.TX_TOPIC) or len(self.TX_TOPIC) == 0):
  58. self.logger.debug("Unpacking content_key \"%s\" from message.", content_key)
  59. if is_json(message.payload):
  60. data = json.loads(message.payload)
  61. if type(data) is dict:
  62. for key in data:
  63. self.set(key, self.__device_to_instance_filter__(key, data[key]), message.mid)
  64. else:
  65. self.set(content_key, self.__device_to_instance_filter__(content_key, data), message.mid)
  66. # String
  67. else:
  68. self.set(content_key, self.__device_to_instance_filter__(content_key, message.payload.decode('utf-8')), message.mid)
  69. else:
  70. self.logger.debug("Ignoring topic %s", content_key)
  71. def __device_to_instance_filter__(self, key, data):
  72. if key in self.RX_FILTER_DATA_KEYS:
  73. if data in [1, 'on', 'ON']:
  74. return True
  75. elif data in [0, 'off', 'OFF']:
  76. return False
  77. return data
  78. def __instance_to_device_filter__(self, key, data):
  79. if key in self.TX_FILTER_DATA_KEYS:
  80. if data is True:
  81. return "on"
  82. elif data is False:
  83. return "off"
  84. return data
  85. def send_command(self, key, data):
  86. data = self.__instance_to_device_filter__(key, data)
  87. if self.TX_TOPIC is not None:
  88. if self.TX_TYPE < 0:
  89. self.logger.error("Unknown tx type. Set TX_TYPE of class to a known value")
  90. else:
  91. self.logger.debug("Sending data for %s - %s", key, str(data))
  92. if self.TX_TYPE == self.TX_DICT:
  93. try:
  94. self.mqtt_client.send('/'.join([self.topic, self.TX_TOPIC]), json.dumps({key: data}))
  95. except TypeError:
  96. print(self.topic)
  97. print(key.__dict__)
  98. print(key)
  99. print(data)
  100. raise TypeError
  101. else:
  102. if type(data) not in [str, bytes]:
  103. data = json.dumps(data)
  104. self.mqtt_client.send('/'.join([self.topic, key, self.TX_TOPIC] if len(self.TX_TOPIC) > 0 else [self.topic, key]), data)
  105. else:
  106. self.logger.error("Unknown tx toptic. Set TX_TOPIC of class to a known value")
  107. class base_rpc(mqtt_base):
  108. SRC_RESPONSE = "/response"
  109. SRC_NULL = "/null"
  110. #
  111. EVENTS_TOPIC = "/events/rpc"
  112. TX_TOPIC = "/rpc"
  113. RESPONSE_TOPIC = SRC_RESPONSE + "/rpc"
  114. NULL_TOPIC = SRC_NULL + "/rpc"
  115. #
  116. RPC_ID_GET_STATUS = 1
  117. RPC_ID_SET = 1734
  118. #
  119. def __init__(self, mqtt_client, topic):
  120. super().__init__(mqtt_client, topic, default_values=dict.fromkeys(self.RX_KEYS))
  121. # data storage
  122. self.__cfg_by_mid__ = None
  123. # initialisations
  124. mqtt_client.add_callback(topic=self.topic, callback=self.receive_callback)
  125. mqtt_client.add_callback(topic=self.topic+"/#", callback=self.receive_callback)
  126. #
  127. self.add_callback(None, None, self.__state_logging__, on_change_only=False)
  128. #
  129. self.rpc_get_status()
  130. def receive_callback(self, client, userdata, message):
  131. data = json.loads(message.payload)
  132. #
  133. if message.topic == self.topic + self.EVENTS_TOPIC:
  134. self.events(data)
  135. elif message.topic == self.topic + self.RESPONSE_TOPIC:
  136. self.response(data)
  137. elif message.topic == self.topic + self.NULL_TOPIC or message.topic == self.topic + self.TX_TOPIC or message.topic == self.topic + "/online":
  138. pass # Ignore response
  139. else:
  140. self.logger.warning("Unexpected message received: %s::%s", message.topic, json.dumps(data, sort_keys=True, indent=4))
  141. def events(self, data):
  142. for rx_key in data["params"]:
  143. if rx_key == "events":
  144. for evt in data["params"]["events"]:
  145. key = evt["component"]
  146. event = evt["event"]
  147. if key in self.RX_KEYS:
  148. if event == "btn_down":
  149. self.set(key, True)
  150. elif event == "btn_up":
  151. self.set(key, False)
  152. else:
  153. key = key + ":" + event
  154. if key in self.RX_KEYS:
  155. self.set(key, True)
  156. else:
  157. self.logger.warning("Unexpected event with data=%s", json.dumps(data, sort_keys=True, indent=4))
  158. elif rx_key in self.RX_KEYS:
  159. state = data["params"][rx_key].get("output")
  160. if state is not None:
  161. self.set(rx_key, state)
  162. def response(self, data):
  163. try:
  164. rpc_id = data.get("id")
  165. except AttributeError:
  166. rpc_id = None
  167. try:
  168. rpc_method = data.get("method")
  169. except AttributeError:
  170. rpc_method = None
  171. if rpc_id == self.RPC_ID_GET_STATUS:
  172. #
  173. # Shelly.GetStatus
  174. #
  175. for rx_key in data.get("result", []):
  176. if rx_key in self.RX_KEYS:
  177. key_data = data["result"][rx_key]
  178. state = key_data.get("output", key_data.get("state"))
  179. if state is not None:
  180. self.set(rx_key, state)
  181. else:
  182. self.logger.warning("Unexpected response with data=%s", json.dumps(data, sort_keys=True, indent=4))
  183. def rpc_tx(self, **kwargs):
  184. if not "id" in kwargs:
  185. raise AttributeError("'id' is missing in keyword arguments")
  186. self.mqtt_client.send(self.topic + self.TX_TOPIC, json.dumps(kwargs))
  187. def rpc_get_status(self):
  188. self.rpc_tx(
  189. id=self.RPC_ID_GET_STATUS,
  190. src=self.topic + self.SRC_RESPONSE,
  191. method="Shelly.GetStatus"
  192. )
  193. def rpc_switch_set(self, key, state: bool):
  194. self.rpc_tx(
  195. id=self.RPC_ID_SET,
  196. src=self.topic + self.SRC_NULL,
  197. method="Switch.Set",
  198. params={"id": int(key[-1]), "on": state}
  199. )
  200. class base_output(base):
  201. def __init__(self, mqtt_client, topic):
  202. super().__init__(mqtt_client, topic)
  203. self.__all_off_enabled__ = True
  204. def disable_all_off(self, state=True):
  205. self.__all_off_enabled__ = not state
  206. def all_off(self):
  207. if self.__all_off_enabled__:
  208. try:
  209. self.__all_off__()
  210. except (AttributeError, TypeError) as e:
  211. self.logger.warning("Method all_off was used, but __all_off__ method wasn't callable: %s", repr(e))