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.

407 lines
13KB

  1. import json
  2. import logging
  3. import logging.config
  4. import os
  5. import sqlite3
  6. import uuid
  7. import yaml
  8. from datetime import datetime
  9. from functools import wraps, update_wrapper
  10. from importlib import import_module
  11. from time import localtime, strftime
  12. import time
  13. import os.path
  14. from flask import Flask, redirect, json, g, make_response
  15. from flask_socketio import SocketIO
  16. from baseapi import *
  17. from db import DBModel
  18. from modules.core.basetypes import Sensor, Actor
  19. from modules.database.dbmodel import Kettle
  20. class ComplexEncoder(json.JSONEncoder):
  21. def default(self, obj):
  22. try:
  23. if isinstance(obj, DBModel):
  24. return obj.__dict__
  25. elif isinstance(obj, Actor):
  26. return {"state": obj.value}
  27. elif isinstance(obj, Sensor):
  28. return {"value": obj.value, "unit": obj.unit}
  29. elif hasattr(obj, "callback"):
  30. return obj()
  31. else:
  32. return None
  33. return None
  34. except TypeError as e:
  35. pass
  36. return None
  37. class Addon(object):
  38. def __init__(self, cbpi):
  39. self.step = StepAPI(cbpi)
  40. self.actor = ActorAPI(cbpi)
  41. self.sensor = SensorAPI(cbpi)
  42. self.kettle = KettleAPI(cbpi)
  43. self.fermenter = FermenterAPI(cbpi)
  44. self.core = CoreAPI(cbpi)
  45. def init(self):
  46. self.core.init()
  47. self.step.init()
  48. self.actor.init()
  49. self.sensor.init()
  50. # self.kettle.init()
  51. # self.fermenter.init()
  52. class ActorCore(object):
  53. key = "actor_types"
  54. def __init__(self, cbpi):
  55. self.logger = logging.getLogger(__name__)
  56. self.cbpi = cbpi
  57. self.cbpi.cache["actors"] = {}
  58. self.cbpi.cache[self.key] = {}
  59. def init(self):
  60. for key, value in self.cbpi.cache["actors"].iteritems():
  61. self.init_one(key)
  62. def init_one(self, id):
  63. try:
  64. actor = self.cbpi.cache["actors"][id]
  65. clazz = self.cbpi.cache[self.key].get(actor.type)["class"]
  66. cfg = actor.config.copy()
  67. cfg.update(dict(cbpi=self.cbpi, id=id))
  68. self.cbpi.cache["actors"][id].instance = clazz(**cfg)
  69. actor.state = 0
  70. actor.power = 100
  71. self.cbpi.emit("INIT_ACTOR", id=id)
  72. except Exception as e:
  73. self.logger.error(e)
  74. def stop_one(self, id):
  75. self.cbpi.cache["actors"][id]["instance"].stop()
  76. self.cbpi.emit("STOP_ACTOR", id=id)
  77. def on(self, id, power=100):
  78. try:
  79. actor = self.cbpi.cache["actors"].get(int(id))
  80. actor.instance.on()
  81. actor.state = 1
  82. actor.power = power
  83. self.cbpi.ws_emit("SWITCH_ACTOR", actor)
  84. self.cbpi.emit("SWITCH_ACTOR_ON", id=id, power=power)
  85. return True
  86. except Exception as e:
  87. self.logger.error(e)
  88. return False
  89. def off(self, id):
  90. try:
  91. actor = self.cbpi.cache["actors"].get(int(id))
  92. actor.instance.off()
  93. actor.state = 0
  94. self.cbpi.ws_emit("SWITCH_ACTOR", actor)
  95. self.cbpi.emit("SWITCH_ACTOR_OFF", id=id)
  96. return True
  97. except Exception as e:
  98. self.logger.error(e)
  99. return False
  100. def toggle(self, id):
  101. if self.cbpi.cache.get("actors").get(id).state == 0:
  102. self.on(id)
  103. else:
  104. self.off(id)
  105. def power(self, id, power):
  106. try:
  107. actor = self.cbpi.cache["actors"].get(int(id))
  108. actor.instance.power(power)
  109. actor.power = power
  110. self.cbpi.ws_emit("SWITCH_ACTOR", actor)
  111. self.cbpi.emit("SWITCH_ACTOR_POWER_CHANGE", id=id, power=power)
  112. return True
  113. except Exception as e:
  114. self.logger.error(e)
  115. return False
  116. def action(self, id, method):
  117. self.cbpi.cache.get("actors").get(id).instance.__getattribute__(method)()
  118. def toggle_timeout(self, id, seconds):
  119. def toggle( id, seconds):
  120. self.cbpi.cache.get("actors").get(int(id)).timer = int(time.time()) + int(seconds)
  121. self.toggle(int(id))
  122. self.cbpi.sleep(seconds)
  123. self.cbpi.cache.get("actors").get(int(id)).timer = None
  124. self.toggle(int(id))
  125. job = self.cbpi._socketio.start_background_task(target=toggle, id=id, seconds=seconds)
  126. def get_state(self, actor_id):
  127. pass
  128. class SensorCore(object):
  129. key = "sensor_types"
  130. def __init__(self, cbpi):
  131. self.logger = logging.getLogger(__name__)
  132. self.cbpi = cbpi
  133. self.cbpi.cache["sensors"] = {}
  134. self.cbpi.cache["sensor_instances"] = {}
  135. self.cbpi.cache["sensor_types"] = {}
  136. def init(self):
  137. for key, value in self.cbpi.cache["sensors"].iteritems():
  138. self.init_one(key)
  139. def init_one(self, id):
  140. try:
  141. sensor = self.cbpi.cache["sensors"][id]
  142. clazz = self.cbpi.cache[self.key].get(sensor.type)["class"]
  143. cfg = sensor.config.copy()
  144. cfg.update(dict(cbpi=self.cbpi, id=id))
  145. self.cbpi.cache["sensors"][id].instance = clazz(**cfg)
  146. self.cbpi.cache["sensors"][id].instance.init()
  147. self.cbpi.emit("INIT_SENSOR", id=id)
  148. def job(obj):
  149. obj.execute()
  150. t = self.cbpi._socketio.start_background_task(target=job, obj=self.cbpi.cache["sensors"][id].instance)
  151. self.cbpi.emit("INIT_SENSOR", id=id)
  152. except Exception as e:
  153. self.logger.error(e)
  154. def stop_one(self, id):
  155. self.cbpi.cache["sensors"][id].instance.stop()
  156. self.cbpi.emit("STOP_SENSOR", id=id)
  157. def get_value(self, sensorid):
  158. try:
  159. return self.cbpi.cache["sensors"][sensorid].instance.value
  160. except:
  161. return None
  162. def get_state(self, actor_id):
  163. pass
  164. def write_log(self, id, value, prefix="sensor"):
  165. filename = "./logs/%s_%s.log" % (prefix, str(id))
  166. formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime())
  167. msg = str(formatted_time) + "," + str(value) + "\n"
  168. with open(filename, "a") as file:
  169. file.write(msg)
  170. def action(self, id, method):
  171. self.cbpi.cache.get("sensors").get(id).instance.__getattribute__(method)()
  172. class BrewingCore(object):
  173. def __init__(self, cbpi):
  174. self.cbpi = cbpi
  175. self.cbpi.cache["step_types"] = {}
  176. self.cbpi.cache["controller_types"] = {}
  177. def log_action(self, text):
  178. filename = "./logs/action.log"
  179. formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime())
  180. with open(filename, "a") as file:
  181. text = text.encode("utf-8")
  182. file.write("%s,%s\n" % (formatted_time, text))
  183. def get_controller(self, name):
  184. return self.cbpi.cache["controller_types"].get(name)
  185. def set_target_temp(self, id, temp):
  186. self.cbpi.cache.get("kettle")[id].target_temp = float(temp)
  187. Kettle.update(**self.cbpi.cache.get("kettle")[id].__dict__)
  188. self.cbpi.ws_emit("UPDATE_KETTLE_TARGET_TEMP", {"id": id, "target_temp": temp})
  189. self.cbpi.emit("SET_KETTLE_TARGET_TEMP", id=id, temp=temp)
  190. def toggle_automatic(self, id):
  191. kettle = self.cbpi.cache.get("kettle")[id]
  192. if kettle.state is False:
  193. # Start controller
  194. if kettle.logic is not None:
  195. cfg = kettle.config.copy()
  196. cfg.update(dict(api=self.cbpi, kettle_id=kettle.id, heater=kettle.heater, sensor=kettle.sensor))
  197. instance = self.get_controller(kettle.logic).get("class")(**cfg)
  198. instance.init()
  199. kettle.instance = instance
  200. def run(instance):
  201. instance.run()
  202. t = self.cbpi._socketio.start_background_task(target=run, instance=instance)
  203. kettle.state = not kettle.state
  204. self.cbpi.ws_emit("UPDATE_KETTLE", self.cbpi.cache.get("kettle").get(id))
  205. self.cbpi.emit("KETTLE_CONTROLLER_STARTED", id=id)
  206. else:
  207. # Stop controller
  208. kettle.instance.stop()
  209. kettle.state = not kettle.state
  210. self.cbpi.ws_emit("UPDATE_KETTLE", self.cbpi.cache.get("kettle").get(id))
  211. self.cbpi.emit("KETTLE_CONTROLLER_STOPPED", id=id)
  212. class FermentationCore(object):
  213. def __init__(self, cbpi):
  214. self.cbpi = cbpi
  215. self.cbpi.cache["fermenter"] = {}
  216. self.cbpi.cache["fermentation_controller_types"] = {}
  217. def get_controller(self, name):
  218. return self.cbpi.cache["fermentation_controller_types"].get(name)
  219. class CraftBeerPI(object):
  220. _logger_configuration_file = './config/logger.yaml'
  221. cache = {}
  222. eventbus = {}
  223. def __init__(self):
  224. if os.path.isfile(self._logger_configuration_file):
  225. logging.config.dictConfig(yaml.load(open(self._logger_configuration_file, 'r')))
  226. self.logger = logging.getLogger(__name__)
  227. self.logger.info("Logger got initialized.")
  228. self.cache["messages"] = []
  229. self.cache["version"] = "3.1"
  230. self.modules = {}
  231. self.addon = Addon(self)
  232. self.actor = ActorCore(self)
  233. self.sensor = SensorCore(self)
  234. self.brewing = BrewingCore(self)
  235. self.fermentation = FermentationCore(self)
  236. self._app = Flask(__name__)
  237. self._app.secret_key = 'Cr4ftB33rP1'
  238. self._app.json_encoder = ComplexEncoder
  239. self._socketio = SocketIO(self._app, json=json, logging=False)
  240. @self._app.route('/')
  241. def index():
  242. return redirect('ui')
  243. def run(self):
  244. self.__init_db()
  245. self.loadPlugins()
  246. self.addon.init()
  247. self.sensor.init()
  248. self.actor.init()
  249. self.beep()
  250. try:
  251. port = int(cbpi.get_config_parameter('port', '5000'))
  252. except ValueError:
  253. port = 5000
  254. self.logger.info("port [%s]", port)
  255. self._socketio.run(self._app, host='0.0.0.0', port=port)
  256. def beep(self):
  257. self.buzzer.beep()
  258. def sleep(self, seconds):
  259. self._socketio.sleep(seconds)
  260. def notify(self, headline, message, type="success", timeout=5000):
  261. msg = {"id": str(uuid.uuid1()), "type": type, "headline": headline, "message": message, "timeout": timeout}
  262. self.ws_emit("NOTIFY", msg)
  263. def ws_emit(self, key, data):
  264. self._socketio.emit(key, data, namespace='/brew')
  265. def __init_db(self, ):
  266. with self._app.app_context():
  267. db = self.get_db()
  268. try:
  269. with self._app.open_resource('../../config/schema.sql', mode='r') as f:
  270. db.cursor().executescript(f.read())
  271. db.commit()
  272. except Exception as e:
  273. self.logger.error(e)
  274. pass
  275. def nocache(self, view):
  276. @wraps(view)
  277. def no_cache(*args, **kwargs):
  278. response = make_response(view(*args, **kwargs))
  279. response.headers['Last-Modified'] = datetime.now()
  280. response.headers[
  281. 'Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0'
  282. response.headers['Pragma'] = 'no-cache'
  283. response.headers['Expires'] = '-1'
  284. return response
  285. return update_wrapper(no_cache, view)
  286. def get_db(self):
  287. db = getattr(g, '_database', None)
  288. if db is None:
  289. def dict_factory(cursor, row):
  290. d = {}
  291. for idx, col in enumerate(cursor.description):
  292. d[col[0]] = row[idx]
  293. return d
  294. db = g._database = sqlite3.connect('craftbeerpi.db')
  295. db.row_factory = dict_factory
  296. return db
  297. def add_cache_callback(self, key, method):
  298. method.callback = True
  299. self.cache[key] = method
  300. def get_config_parameter(self, key, default):
  301. cfg = self.cache["config"].get(key)
  302. if cfg is None:
  303. return default
  304. else:
  305. return cfg.value
  306. def emit(self, key, **kwargs):
  307. if self.eventbus.get(key) is not None:
  308. for value in self.eventbus[key]:
  309. if value["async"] is False:
  310. value["function"](**kwargs)
  311. else:
  312. t = self.cbpi._socketio.start_background_task(target=value["function"], **kwargs)
  313. def loadPlugins(self):
  314. for filename in os.listdir("./modules/plugins"):
  315. if os.path.isdir("./modules/plugins/" + filename) is False:
  316. continue
  317. try:
  318. self.modules[filename] = import_module("modules.plugins.%s" % (filename))
  319. except Exception as e:
  320. self.logger.error(e)
  321. self.notify("Failed to load plugin %s " % filename, str(e), type="danger", timeout=None)
  322. cbpi = CraftBeerPI()
  323. addon = cbpi.addon