environmental.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. <?php
  2. /* provides a table to store environmental data submitted by bosch bme sensors
  3. *
  4. * the table radondb.bosch_bme680 stores a timestamp time as seconds since unix epoch,
  5. * the sensor sending the data is stored, eg: 'H' is stored as 72,
  6. * and the sensors readings as integers, [celsius, humidity, pressure as pascals,
  7. * gas resistance, indoor air quality index, and iaq accuracy]
  8. *
  9. * to use as viewer:
  10. * provide name for the Bosch BME680 sensor and optionally a unix epoch range (...)
  11. *
  12. * function name(parameters) returns array:
  13. * get_BME860_humidityData('H', ...): ["time"=>{time}, "reading"=>{reading}, ...]
  14. * get_BME860_celsiusData('H', ...): ["time"=>{time}, "reading"=>{reading}, ...]
  15. * get_BME860_pressureData('H', ...): ["time"=>{time}, "reading"=>{reading}, ...]
  16. * get_BME860_gasData('H', ...): ["time"=>{time}, "reading"=>{reading}, ...]
  17. * get_BME860_iaqData('H', ...): ["time"=>{time}, "reading"=>{reading}, ...]
  18. * get_BME860_iaqAccuracyData('H', ...): ["time"=>{time}, "reading"=>{reading}, ...]
  19. * get_BME860_krumaylData('H', ...): ["time"=>{time}, "reading"=>{reading}, ...]
  20. * get_BME860_ukrumaylData('H', ...): ["time"=>{time}, "reading"=>{reading}, ...]
  21. * get_BME860_wesDust('H', ...): ["time"=>{time}, "reading"=>{reading}, ...]
  22. *
  23. * to use as submitter:
  24. * use HTTP POST, data will be saved to radondb.bosch_bme680
  25. */
  26. define("BME680_COLUMNS", "time sensor celsius humidity pascal gas_resistance indoor_air_quality iaq_accuracy");
  27. function save() {
  28. // connect to mysql database radondb
  29. $db = mysql_connect("localhost", "root", "secom8703");
  30. mysql_select_db("radondb", $db);
  31. $columns = explode(" ", BME680_COLUMNS);
  32. // make sure events table exists
  33. if(false === mysql_query("SELECT 1 FROM bosch_bme680 LIMIT 1", $db)) {
  34. $create_cols = implode(" INTEGER, ", $columns) . " INTEGER";
  35. mysql_query("CREATE TABLE bosch_bme680 ({$create_cols}, CONSTRAINT reading UNIQUE (time, sensor))", $db);
  36. }
  37. // prepare data - first store column names, then match column names to bme680 submitted data
  38. $table_cols = implode(",", $columns);
  39. if(false !== ($key = array_search("celsius", $columns))) $columns[$key] = "temperature";
  40. if(false !== ($key = array_search("pascal", $columns))) $columns[$key] = "pressure";
  41. if(false !== ($key = array_search("indoor_air_quality", $columns))) $columns[$key] = "iaq_(accuracy)";
  42. $values = post2toast($_POST, $columns);
  43. // data should just be a bunch of integers
  44. $values = implode(",", $values);
  45. if(false === mysql_query("INSERT INTO bosch_bme680 ({$table_cols}) VALUES ({$values})", $db)) {
  46. // event previously recorded
  47. } else /* event sucessfully stored */;
  48. // mysql needs clean-up
  49. mysql_close($db);
  50. exit("thanks");
  51. }
  52. // convert alphabetic sensor name to integer [A..Z] to [65..90]
  53. function eMa_sensorName($str) {
  54. return (int) ord(strtoupper($str[0]));
  55. }
  56. function post2toast($post, $columns) {
  57. // either manually provide time or overwrite it
  58. $post["time"] = time();
  59. // convert sensor ASCII character (A..Z) into integer value represented using a string
  60. if(isset($post["sensor"])) $post["sensor"] = strval(eMa_sensorName($post["sensor"][0])) . ' ';
  61. $arr = array();
  62. foreach($columns as $k => $v) {
  63. if(isset($post[$v])) $arr[$k] = trim($post[$v]);
  64. }
  65. // handle splitting iaq into index and accuracy
  66. if(false !== ($key = array_search("iaq_accuracy", $columns))) {
  67. if(false !== ($key2 = array_search("iaq_(accuracy)", $columns))) {
  68. $chomp = strtok($arr[$key2], ' ');
  69. $arr[$key] = trim(strtok(''), " \n()") . /* needs whitespace for later */ ' ';
  70. }
  71. }
  72. // convert all values into integers, round floats
  73. foreach($arr as $k => $v) {
  74. $arr[$k] = (int) round(getFirstWord($v));
  75. }
  76. return $arr;
  77. }
  78. // confusing, word is meant to be first number
  79. function getFirstWord($v) {
  80. $v = str_split(strtok(trim($v), ' '), 1);
  81. $s = "";
  82. foreach($v as $t) {
  83. if('.' === $t) { $s .= '.'; continue; }
  84. if((0x29 < ord($t)) && (0x40 > ord($t))) $s .= $t;
  85. else break;
  86. }
  87. return $s;
  88. }
  89. // save data sent to us (HTTP POST terminate here)
  90. if(!empty($_POST)) save();
  91. // otherwise we are providing some additional functionality ...
  92. define("GET_LAST_READING", PHP_INT_MAX);
  93. function getColumnHelper($type, $sensor = 'H', $queryOptions = NULL) {
  94. /* Prior to using query options start and end could have been used to
  95. * specify sensor samples between two dates, this feature was not really
  96. * used. Query Options overlap this feature. Under normal conditions
  97. * using Query Options also retrieves data from a cache rather than from
  98. * the MySQL database.
  99. *
  100. * using a default QueryOptions object should not use the cache, if you
  101. * want to still make use of start and end, setting them in the QueryOptions
  102. * is supported
  103. */
  104. $start = false;
  105. $end = false;
  106. // expand query options if they are provided
  107. if((NULL !== $queryOptions) and ("QueryOptions" === get_class($queryOptions))) {
  108. if($queryOptions->useCache()) {
  109. $rows = CACHE_DB::getCachedData($sensor, $type,
  110. $queryOptions->zoom,
  111. $queryOptions->fir,
  112. $queryOptions->start,
  113. $queryOptions->end
  114. );
  115. //return $rows;
  116. // add the most recent non-cached bosch bme680 sensor readings [mySQL]
  117. $lastReading = bosch_bme_680_query($type, $sensor, GET_LAST_READING);
  118. if($lastReading) {
  119. if(defined("ARRAY_OF_TIME_READING_PAIRS")) {
  120. $rows[] = array_pop($lastReading);
  121. } else {
  122. $rows[0][] = $lastReading[0]["time"];
  123. $rows[1][] = $lastReading[0]["reading"];
  124. }
  125. }
  126. return $rows;
  127. } // else - possibly get the start/end, use MySQL instead
  128. /* continuing down this path will just use the MySQL database, we
  129. * can try to get the start and end seconds since UNIX epoch from
  130. * the Query Options though...
  131. */
  132. $start = $queryOptions->start;
  133. $end = $queryOptions->end;
  134. } // else
  135. return bosch_bme_680_query($type, $sensor, $start, $end);
  136. }
  137. function bosch_bme_680_query($type, $sensor = 'H', $start = false, $end = false) {
  138. // check if we are only supposed to get last reading, do this before start is modified
  139. $lastReading = GET_LAST_READING === $start;
  140. // create a list of columns that are okay to query
  141. $okay_columns = explode(" ", BME680_COLUMNS);
  142. if(false !== ($k = array_search("time", $okay_columns))) unset($okay_columns[$k]);
  143. if(false !== ($k = array_search("sensor", $okay_columns))) unset($okay_columns[$k]);
  144. // cancel this query if type is invalid
  145. if(false === array_search($type, $okay_columns)) {
  146. error_log("the requested type, {$type} must be one of: " . implode(',', $okay_columns));
  147. return array(/* empty array for you */);
  148. }
  149. // the sensor that you want has a different name, I will look it up
  150. $sensor = strval(eMa_sensorName($sensor));
  151. # connect to mysql database radondb
  152. $db = mysql_connect("localhost", "root", "secom8703");
  153. mysql_select_db("radondb", $db);
  154. // prevent injection in time interval input
  155. $start = intval($start);
  156. $end = intval($end);
  157. $and = (/* php zero is false */ $start and $end) ?"time >= {$start} AND time <= {$end} AND " :"";
  158. // add optional time interval to query
  159. $sql = "SELECT time, {$type} AS reading FROM `bosch_bme680` WHERE {$and}";
  160. // lookup by sensor, make sure query sorts results oldest event to newest
  161. $sql .= "sensor = {$sensor} ORDER BY time ASC";
  162. if($lastReading) {
  163. $sql = "SELECT time, {$type} AS reading FROM `bosch_bme680` WHERE ";
  164. $sql .= "sensor = {$sensor} ORDER BY time DESC LIMIT 1";
  165. }
  166. if(false === ($result = mysql_query($sql, $db))) {
  167. fwrite(STDERR, "{$type} query failed:" . PHP_EOL . mysql_error($db));
  168. return false;
  169. }
  170. $rows = array();
  171. while($row = mysql_fetch_array($result, MYSQL_ASSOC)) {
  172. $rows[] = $row;
  173. //printRow($row);
  174. }
  175. mysql_free_result($result);
  176. # mysql needs clean-up
  177. mysql_close($db);
  178. return $rows;
  179. }
  180. // returns humidity percentage readings, with optional time range (unix epoch)
  181. function get_BME860_humidityData($sensor = 'H', $qo = NULL) {
  182. return getColumnHelper("humidity", $sensor, $qo);
  183. }
  184. // returns temperature Celsius readings, with optional time range (unix epoch)
  185. function get_BME860_celsiusData($sensor = 'H', $qo = NULL) {
  186. return getColumnHelper("celsius", $sensor, $qo);
  187. }
  188. // returns pressure pascal readings, with optional time range (unix epoch)
  189. function get_BME860_pressureData($sensor = 'H', $qo = NULL) {
  190. return getColumnHelper("pascal", $sensor, $qo);
  191. }
  192. // returns gas resistance readings, with optional time range (unix epoch)
  193. function get_BME860_gasData($sensor = 'H', $qo = NULL) {
  194. return getColumnHelper("gas_resistance", $sensor, $qo);
  195. }
  196. // returns indoor air quality index readings, with optional time range (unix epoch)
  197. function get_BME860_iaqData($sensor = 'H', $qo = NULL) {
  198. return getColumnHelper("indoor_air_quality", $sensor, $qo);
  199. }
  200. // returns IAQ accuracy readings, with optional time range (unix epoch)
  201. function get_BME860_iaqAccuracyData($sensor = 'H', $qo = NULL) {
  202. return getColumnHelper("iaq_accuracy", $sensor, $qo);
  203. }
  204. // returns temperature Fahrenheit readings, with optional time range (Unix epoch)
  205. function get_BME860_fahrenheitData($sensor = 'H', $qo = NULL) {
  206. $arr = getColumnHelper("celsius", $sensor, $qo);
  207. array_walk($arr, function(&$v) { $v["reading"] = 32 + round($v["reading"] * 9 / 5); });
  208. return $arr;
  209. }
  210. // crop integer values to 0..100
  211. function kug($x) { return $x > 0 ?$x < 100 ?$x :100 :0; }
  212. /* returns realative pressure readings, with optional time range (unix epoch)
  213. *
  214. * - a common elevation for people living in Iowa might be 270 meters above sea level.
  215. * - the highest pressure in Iowa occurred in 1989 and was recorded as 105452Pa
  216. * - the lowest pressure in Iowa occurred in 1940 and was recorded as 98408Pa
  217. * - in the eye of a hurricane or tornado it is possible to have a pressure of 87000Pa
  218. * - at the Dead Sea, at the lowest place on earth, 430 meters below sea level, and this
  219. * one weird time in 1961, you could get lucky and experience a pressure of 108100Pa,
  220. * normally it is 106500Pa.
  221. * - sea level pressure is 101325Pa, or one atmosphere (atm)
  222. * - the pressure at the peak of mt everest might be as low as 33500Pa
  223. *
  224. * "altitude (km) versus pressure (kPa)"
  225. * |
  226. * |
  227. * 25k-| *
  228. * |
  229. * |
  230. * 20k-| '*' represents average atmospheric pressure
  231. * | * '-' line points to peak of Mt Everest, highest
  232. * | above sea montain with an altiture of 8.8km
  233. * 15k-| * and an average pressure of 33700Pa
  234. * |
  235. * | *
  236. * 10k-| *
  237. * |----------.*
  238. * | ;'` '.*
  239. * 5km-| .`;.' `. *
  240. * |.`Mount`. `, *
  241. * | Everest , *
  242. * 0km |_____________________________*____
  243. * 0 10kPa 35kPa 70kPa 101.3kPa
  244. *
  245. * the krumayl is an experimental unit that is more representative of
  246. * pressure that one may experience in everyday use:
  247. * to increase sensitivity, a pressure of 108100Pa is going to be one hundred,
  248. * a pressure of 87000 will be zero. To scale existing readings:
  249. * (x - 87000) * (100 - 0) / (108100 - 87000) + 0 or simply (x - 87000)/211
  250. * a more sensitive unit, ukrumayl only considers pressures between 96kPa and 106kPa
  251. * (x - 96000) * (100 - 0) / (106000 - 96000) + 0 or simply (x - 96000)/100
  252. */
  253. function get_BME860_krumaylData($sensor = 'H', $qo = NULL) {
  254. $arr = getColumnHelper("pascal", $sensor, $qo);
  255. array_walk($arr, function(&$v) { $v["reading"] = kug(round(($v["reading"] - 87000)/211)); });
  256. return $arr;
  257. }
  258. function get_BME860_ukrumaylData($sensor = 'H', $qo = NULL) {
  259. $arr = getColumnHelper("pascal", $sensor, $qo);
  260. array_walk($arr, function(&$v) { $v["reading"] = kug(round(($v["reading"] - 96000)/100)); });
  261. return $arr;
  262. }
  263. // experimental and not well thought out unit representing gas resistance
  264. function get_BME860_wesDust($sensor = 'H', $qo = NULL) {
  265. $arr = getColumnHelper("gas_resistance", $sensor, $qo);
  266. array_walk($arr, function(&$v) { $v["reading"] = 100-(kug(round(($v["reading"] - 100000)/2000))); });
  267. return $arr;
  268. }
  269. //var_dump(get_BME860_iaqAccuracyData());
  270. //var_dump(get_BME860_iaqData());
  271. //var_dump(get_BME860_celsiusData());
  272. //var_dump(get_BME860_humidityData());
  273. //var_dump(get_BME860_gasData());
  274. //var_dump(get_BME860_pressureData());