wether station gui client
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.

smarthome.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3. #
  4. import config
  5. from rpi_envsens.dht import dht_22
  6. from rpi_envsens.bmp import bmp_180
  7. import helpers
  8. import garage_protocol
  9. import gui
  10. import logging
  11. import os
  12. import report
  13. import socket_protocol
  14. import task
  15. import tcp_socket
  16. import time
  17. import wetation_protocol
  18. import wx
  19. ############################################################################################################################
  20. # #
  21. # Application Structure #
  22. # #
  23. # +-----------------------------+ +------------------------------+ +---------------------------+ #
  24. # | Data Request Task (1s) | | Data Receive Callback | | GUI Update WX-IDLE-TASK | #
  25. # | Counter 0-9 | | | | | #
  26. # | | | Append data to ValueStorage | | - Update all GUI Elements | #
  27. # | *: Request door_pos | +------------------------------+ | - Check Data Status to | #
  28. # | 0: Request env-data-in bmp | | mark all fragile con- | #
  29. # | 1: Request env-data-in dht | | tent | #
  30. # | 5: Request env-data-out bmp | +---------------------------+ #
  31. # | 6: Request env-data-out dht | #
  32. # | 9: Reconnect if needed | #
  33. # +-----------------------------+ #
  34. # #
  35. ############################################################################################################################
  36. try:
  37. from config import APP_NAME as ROOT_LOGGER_NAME
  38. except ImportError:
  39. ROOT_LOGGER_NAME = 'root'
  40. logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
  41. class ValueStorage(dict):
  42. MAX_AGE = 60 # Seconds
  43. DATA_KEY_GATE_POSITION = 'gate_position'
  44. DATA_KEY_HUMIDITY_IN = 'humidity_in'
  45. DATA_KEY_HUMIDITY_OUT = 'humidity_out'
  46. DATA_KEY_PRESSURE_IN = 'pressure_in'
  47. DATA_KEY_PRESSURE_OUT = 'pressure_out'
  48. DATA_KEY_TEMPERATURE_IN = 'temp_in'
  49. DATA_KEY_TEMPERATURE_OUT = 'temp_out'
  50. DATA_KEY_2ND_TEMPERATURE_IN = '2nd_temp_in'
  51. DATA_KEY_2ND_TEMPERATURE_OUT = '2nd_temp_out'
  52. FORMATTER_TEMP = ('%.1f °C', '-.- °C')
  53. FORMATTER_HUMI = ('%.1f %%', '-.- %')
  54. FORMATTER_PRES = ('%4d hPa', '- hPa')
  55. DATA_KEYS_CONT_STAT = [DATA_KEY_HUMIDITY_IN, DATA_KEY_HUMIDITY_OUT, DATA_KEY_PRESSURE_IN, DATA_KEY_PRESSURE_OUT, DATA_KEY_TEMPERATURE_IN, DATA_KEY_TEMPERATURE_OUT, DATA_KEY_2ND_TEMPERATURE_IN, DATA_KEY_2ND_TEMPERATURE_OUT]
  56. def append(self, drk, value):
  57. tm = time.time()
  58. logger.info('Value stored (%s): %s', drk, repr(value))
  59. if drk in self.DATA_KEYS_CONT_STAT:
  60. self[drk] = (value.mean, tm)
  61. if drk + '.min' not in self or value.min < self[drk + '.min'][0]:
  62. self[drk + '.min'] = (value.min, tm)
  63. if drk + '.max' not in self or value.max < self[drk + '.max'][0]:
  64. self[drk + '.max'] = (value.max, tm)
  65. else:
  66. self[drk] = (value, tm)
  67. def reset_min_max(self, data_key):
  68. del self[data_key + '.min']
  69. del self[data_key + '.max']
  70. def __get_formatter__(self, key):
  71. return {
  72. self.DATA_KEY_2ND_TEMPERATURE_IN: self.FORMATTER_TEMP,
  73. self.DATA_KEY_2ND_TEMPERATURE_OUT: self.FORMATTER_TEMP,
  74. self.DATA_KEY_HUMIDITY_IN: self.FORMATTER_HUMI,
  75. self.DATA_KEY_HUMIDITY_OUT: self.FORMATTER_HUMI,
  76. self.DATA_KEY_PRESSURE_IN: self.FORMATTER_PRES,
  77. self.DATA_KEY_PRESSURE_OUT: self.FORMATTER_PRES,
  78. self.DATA_KEY_TEMPERATURE_IN: self.FORMATTER_TEMP,
  79. self.DATA_KEY_TEMPERATURE_OUT: self.FORMATTER_TEMP,
  80. }.get(key)
  81. def get_validated(self, key, value_type=None, default=None):
  82. tm = time.time()
  83. fallback = {
  84. self.DATA_KEY_TEMPERATURE_IN: self.DATA_KEY_2ND_TEMPERATURE_IN,
  85. self.DATA_KEY_TEMPERATURE_OUT: self.DATA_KEY_2ND_TEMPERATURE_OUT,
  86. }.get(key)
  87. if value_type in ['min', 'max']:
  88. key = key + '.' + value_type
  89. value = dict.get(self, key)
  90. if value is None and fallback is not None:
  91. value = dict.get(self, fallback)
  92. if value is None:
  93. return default
  94. if tm - value[1] <= self.MAX_AGE:
  95. if key == self.DATA_KEY_GATE_POSITION:
  96. return int(value[0] * 50)
  97. else:
  98. return value[0]
  99. else:
  100. return default
  101. def get(self, key, value_type=None, formatted=True, default=None):
  102. formatter = self.__get_formatter__(key)
  103. value = self.get_validated(key, value_type, default=default)
  104. if not formatted:
  105. return value
  106. else:
  107. if formatter is None:
  108. return default
  109. else:
  110. try:
  111. return formatter[0] % value
  112. except Exception:
  113. return formatter[1]
  114. class Wetation(gui.Wetation):
  115. SRC_ID_GARAGE = 0
  116. SRC_ID_IN = 1
  117. SRC_ID_OUT = 2
  118. ALL_SRC_IDS = [SRC_ID_GARAGE, SRC_ID_IN, SRC_ID_OUT, ]
  119. SRC_NAMES = {
  120. SRC_ID_GARAGE: u'garage',
  121. SRC_ID_IN: u'wet-in',
  122. SRC_ID_OUT: u'wet-out',
  123. }
  124. SRC_IPS = {
  125. SRC_ID_GARAGE: config.server_garage_ip,
  126. SRC_ID_IN: config.server_in_ip,
  127. SRC_ID_OUT: config.server_out_ip,
  128. }
  129. SRC_PORTS = {
  130. SRC_ID_GARAGE: config.server_garage_port,
  131. SRC_ID_IN: config.server_in_port,
  132. SRC_ID_OUT: config.server_out_port,
  133. }
  134. SRC_SECRETS = {
  135. SRC_ID_GARAGE: config.server_garage_secret,
  136. SRC_ID_IN: config.server_in_secret,
  137. SRC_ID_OUT: config.server_out_secret,
  138. }
  139. SRC_CLASSES = {
  140. SRC_ID_GARAGE: garage_protocol.my_base_protocol_tcp,
  141. SRC_ID_IN: wetation_protocol.my_base_protocol_tcp,
  142. SRC_ID_OUT: wetation_protocol.my_base_protocol_tcp,
  143. }
  144. def __init__(self, *args, **kwds):
  145. gui.Wetation.__init__(self, *args, **kwds)
  146. self.Bind(wx.EVT_IDLE, self.gui_update)
  147. self.in_temperature_min.Bind(wx.EVT_LEFT_DOWN, self.reset_in_temp_minmax_evt)
  148. self.in_temperature_max.Bind(wx.EVT_LEFT_DOWN, self.reset_in_temp_minmax_evt)
  149. self.out_temperature_min.Bind(wx.EVT_LEFT_DOWN, self.reset_out_temp_minmax_evt)
  150. self.out_temperature_max.Bind(wx.EVT_LEFT_DOWN, self.reset_out_temp_minmax_evt)
  151. self.ShowFullScreen(config.FULL_SCREEN)
  152. #
  153. self.__src__ = {}
  154. self.__value_storage__ = ValueStorage()
  155. self.__init_communication__()
  156. #
  157. self.__drt_cnt__ = -1
  158. self.__task_1s__ = task.periodic(1, self.data_request_task)
  159. def __init_communication__(self):
  160. #
  161. # Start TCP-Clients
  162. for src_id in self.ALL_SRC_IDS:
  163. cn = self.SRC_NAMES[src_id]
  164. c_tcp = tcp_socket.tcp_client_stp(self.SRC_IPS[src_id], self.SRC_PORTS[src_id], channel_name=cn)
  165. self.__src__[src_id] = self.SRC_CLASSES[src_id](c_tcp, secret=self.SRC_SECRETS[src_id], auto_auth=True, channel_name=cn)
  166. #
  167. self.__src__[src_id].register_callback(None, None, self.data_receive_callback, src_id)
  168. def gate_oc_evt(self, event):
  169. r = wx.MessageDialog(
  170. self,
  171. "Soll das Garagentor betätigt werden?",
  172. "Garage",
  173. wx.YES_NO | wx.NO_DEFAULT | wx.ICON_WARNING
  174. ).ShowModal()
  175. if r == wx.ID_YES:
  176. logger.info("Gate open/close request")
  177. self.__src__[self.SRC_ID_GARAGE].send(socket_protocol.SID_EXECUTE_REQUEST, garage_protocol.OPEN_CLOSE_GATE, None)
  178. event.Skip()
  179. def reset_in_temp_minmax_evt(self, event):
  180. self.__value_storage__.reset_min_max(ValueStorage.DATA_KEY_TEMPERATURE_IN)
  181. self.__value_storage__.reset_min_max(ValueStorage.DATA_KEY_2ND_TEMPERATURE_IN)
  182. event.Skip()
  183. def reset_out_temp_minmax_evt(self, event):
  184. self.__value_storage__.reset_min_max(ValueStorage.DATA_KEY_TEMPERATURE_OUT)
  185. self.__value_storage__.reset_min_max(ValueStorage.DATA_KEY_2ND_TEMPERATURE_OUT)
  186. event.Skip()
  187. def data_request_task(self, rt):
  188. (rt)
  189. if self.__drt_cnt__ < 9:
  190. self.__drt_cnt__ += 1
  191. else:
  192. self.__drt_cnt__ = 0
  193. logger.debug('data_request_task stated with cnt = %d', self.__drt_cnt__)
  194. #
  195. # Request GATE_POSITION
  196. #
  197. self.__src__[self.SRC_ID_GARAGE].send(service_id=socket_protocol.SID_READ_REQUEST, data_id=garage_protocol.GATE_POSITION, data=None)
  198. #
  199. # Request WET-OUT-DATA
  200. #
  201. if self.__drt_cnt__ == 0:
  202. self.__src__[self.SRC_ID_OUT].send(service_id=socket_protocol.SID_READ_REQUEST, data_id=wetation_protocol.ENVDATA_STATISTIC_BMP, data=None)
  203. elif self.__drt_cnt__ == 1:
  204. self.__src__[self.SRC_ID_OUT].send(service_id=socket_protocol.SID_READ_REQUEST, data_id=wetation_protocol.ENVDATA_STATISTIC_DHT, data=None)
  205. #
  206. # Request WET-IN-DATA
  207. #
  208. elif self.__drt_cnt__ == 5:
  209. self.__src__[self.SRC_ID_IN].send(service_id=socket_protocol.SID_READ_REQUEST, data_id=wetation_protocol.ENVDATA_STATISTIC_BMP, data=None)
  210. elif self.__drt_cnt__ == 6:
  211. self.__src__[self.SRC_ID_IN].send(service_id=socket_protocol.SID_READ_REQUEST, data_id=wetation_protocol.ENVDATA_STATISTIC_DHT, data=None)
  212. elif self.__drt_cnt__ == 9:
  213. self.__reconnect__()
  214. #
  215. def __reconnect__(self):
  216. for src_id in self.ALL_SRC_IDS:
  217. if not self.__src__[src_id].is_connected():
  218. logger.warning("Trying to reconnect prot_id %s", self.SRC_NAMES.get(src_id, 'Unknown'))
  219. self.__src__[src_id].reconnect()
  220. def __data_receive_key__(self, src_id, data_id, key=None, default_value=None):
  221. return {
  222. self.SRC_ID_GARAGE: {
  223. garage_protocol.GATE_POSITION: {
  224. None: ValueStorage.DATA_KEY_GATE_POSITION,
  225. }
  226. },
  227. self.SRC_ID_IN: {
  228. wetation_protocol.ENVDATA_STATISTIC_BMP: {
  229. bmp_180.KEY_PRESSURE: ValueStorage.DATA_KEY_PRESSURE_IN,
  230. bmp_180.KEY_TEMPERATURE: ValueStorage.DATA_KEY_2ND_TEMPERATURE_IN
  231. },
  232. wetation_protocol.ENVDATA_STATISTIC_DHT: {
  233. dht_22.KEY_HUMIDITY: ValueStorage.DATA_KEY_HUMIDITY_IN,
  234. dht_22.KEY_TEMPERATURE: ValueStorage.DATA_KEY_TEMPERATURE_IN
  235. },
  236. },
  237. self.SRC_ID_OUT: {
  238. wetation_protocol.ENVDATA_STATISTIC_BMP: {
  239. bmp_180.KEY_PRESSURE: ValueStorage.DATA_KEY_PRESSURE_OUT,
  240. bmp_180.KEY_TEMPERATURE: ValueStorage.DATA_KEY_2ND_TEMPERATURE_OUT
  241. },
  242. wetation_protocol.ENVDATA_STATISTIC_DHT: {
  243. dht_22.KEY_HUMIDITY: ValueStorage.DATA_KEY_HUMIDITY_OUT,
  244. dht_22.KEY_TEMPERATURE: ValueStorage.DATA_KEY_TEMPERATURE_OUT
  245. },
  246. }
  247. }.get(src_id, {}).get(data_id, {}).get(key, default_value)
  248. def data_receive_callback(self, data, src_id):
  249. if src_id in [self.SRC_ID_IN, self.SRC_ID_OUT] and data.get_data_id() in [wetation_protocol.ENVDATA_STATISTIC_DHT, wetation_protocol.ENVDATA_STATISTIC_BMP]:
  250. try:
  251. for key in data.get_data():
  252. if key != bmp_180.KEY_TIME:
  253. drk = self.__data_receive_key__(src_id, data.get_data_id(), key)
  254. if drk is not None:
  255. self.__process_rx_data__(
  256. drk,
  257. helpers.continues_statistic(**data.get_data()[key])
  258. )
  259. else:
  260. self.__warning_rx_data__('Unknown RX-Data', data, src_id)
  261. except TypeError as e:
  262. self.__warning_rx_data__('Wront DataType in RX-Data (%s)' % e, data, src_id)
  263. else:
  264. drk = self.__data_receive_key__(src_id, data.get_data_id())
  265. if drk is not None:
  266. self.__process_rx_data__(
  267. drk,
  268. data.get_data()
  269. )
  270. else:
  271. self.__warning_rx_data__('Unknown RX-Data', data, src_id)
  272. def __process_rx_data__(self, drk, data):
  273. self.__value_storage__.append(drk, data)
  274. def __warning_rx_data__(self, desc, data, src_id):
  275. logger.warning('%s from %s: %s', desc, self.SRC_NAMES.get(src_id, 'Unknown'), repr(data))
  276. def __check_data_status__(self):
  277. if dict.get(self.__value_storage__, ValueStorage.DATA_KEY_TEMPERATURE_IN) is None and dict.get(self.__value_storage__, ValueStorage.DATA_KEY_2ND_TEMPERATURE_IN) is not None:
  278. self.in_temperature.SetBackgroundColour((255, 255, 200, 255))
  279. else:
  280. self.in_temperature.SetBackgroundColour((250, 249, 255, 255))
  281. if dict.get(self.__value_storage__, ValueStorage.DATA_KEY_TEMPERATURE_OUT) is None and dict.get(self.__value_storage__, ValueStorage.DATA_KEY_2ND_TEMPERATURE_OUT) is not None:
  282. self.out_temperature.SetBackgroundColour((255, 255, 200, 255))
  283. else:
  284. self.out_temperature.SetBackgroundColour((250, 249, 255, 255))
  285. if dict.get(self.__value_storage__, ValueStorage.DATA_KEY_GATE_POSITION) is None:
  286. self.heading_garage.SetBackgroundColour((142, 35, 35, 255))
  287. else:
  288. self.heading_garage.SetBackgroundColour((35, 35, 142, 255))
  289. def gui_update(self, event):
  290. #
  291. # TIME and DATE
  292. #
  293. self.time.SetLabel(time.strftime("%H:%M"))
  294. self.date.SetLabel(time.strftime("%d.%m.%Y"))
  295. #
  296. update_list = [
  297. # WET-OUT
  298. (self.out_humidity.SetLabel, [ValueStorage.DATA_KEY_HUMIDITY_OUT, 'mean', True]),
  299. (self.out_pressure.SetLabel, [ValueStorage.DATA_KEY_PRESSURE_OUT, 'mean', True]),
  300. (self.out_temperature.SetLabel, [ValueStorage.DATA_KEY_TEMPERATURE_OUT, 'mean', True]),
  301. (self.out_temperature_max.SetLabel, [ValueStorage.DATA_KEY_TEMPERATURE_OUT, 'max', True]),
  302. (self.out_temperature_min.SetLabel, [ValueStorage.DATA_KEY_TEMPERATURE_OUT, 'min', True]),
  303. # WET-IN
  304. (self.in_humidity.SetLabel, [ValueStorage.DATA_KEY_HUMIDITY_IN, 'mean', True]),
  305. (self.in_pressure.SetLabel, [ValueStorage.DATA_KEY_PRESSURE_IN, 'mean', True]),
  306. (self.in_temperature.SetLabel, [ValueStorage.DATA_KEY_TEMPERATURE_IN, 'mean', True]),
  307. (self.in_temperature_max.SetLabel, [ValueStorage.DATA_KEY_TEMPERATURE_IN, 'max', True]),
  308. (self.in_temperature_min.SetLabel, [ValueStorage.DATA_KEY_TEMPERATURE_IN, 'min', True]),
  309. # GATE
  310. (self.gate_position.SetValue, [ValueStorage.DATA_KEY_GATE_POSITION, None, False, 50])
  311. ]
  312. #
  313. for func, args in update_list:
  314. func(self.__value_storage__.get(*args))
  315. self.__check_data_status__()
  316. #
  317. #
  318. self.Layout()
  319. event.Skip()
  320. def run(self):
  321. self.__task_1s__.run()
  322. def close(self):
  323. self.__task_1s__.stop()
  324. self.__task_1s__.join()
  325. def __del__(self):
  326. self.close()
  327. class MyApp(wx.App):
  328. def OnInit(self):
  329. self.frame = Wetation(None, wx.ID_ANY, "")
  330. self.SetTopWindow(self.frame)
  331. self.frame.Show()
  332. return True
  333. if __name__ == "__main__":
  334. report.appLoggingConfigure(os.path.dirname(__file__), config.LOGTARGET, ((config.APP_NAME, config.LOGLVL), ), fmt=config.formatter, host=config.LOGHOST, port=config.LOGPORT)
  335. #
  336. app = MyApp(0)
  337. app.frame.run()
  338. app.MainLoop()
  339. app.frame.close()