meteostanice.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. import { Database } from "bun:sqlite";
  2. import { nanoid } from 'nanoid'
  3. import generateSecureRandomString from "./generateSecureRandomString";
  4. const meteostaniceDB = new Database("./data/meteostanice.sqlite");
  5. meteostaniceDB.run(`create table if not exists list (
  6. id text not null primary key,
  7. owner text not null,
  8. name text not null,
  9. description text,
  10. websocketKey text not null,
  11. subowners text,
  12. warnings text,
  13. timestamp datetime default current_timestamp
  14. );`)
  15. meteostaniceDB.run(`create table if not exists data (
  16. id text not null primary key,
  17. meteostanica text not null,
  18. timestamp datetime default current_timestamp,
  19. indoorTemp text not null,
  20. indoorPressure text not null,
  21. indoorHumidity text not null,
  22. outdoorConnected integer not null,
  23. outdoorTemp text not null,
  24. outdoorPressure text not null,
  25. outdoorHumidity text not null
  26. );`)
  27. meteostaniceDB.run(`create table if not exists public (
  28. id text not null primary key,
  29. name text not null,
  30. description text,
  31. timestamp datetime default current_timestamp,
  32. showOwner integer default 1,
  33. showIndoorTemp integer default 1,
  34. showIndoorPressure integer default 1,
  35. showIndoorHumidity integer default 1,
  36. showOutdoorConnected integer default 1,
  37. showOutdoorTemp integer default 1,
  38. showOutdoorPressure integer default 1,
  39. showOutdoorHumidity integer default 1
  40. );`)
  41. import nodemailer from "nodemailer"
  42. const transporter = nodemailer.createTransport({
  43. host: process.env.WARNING_EMAIL_SMTP_HOSTNAME,
  44. port: process.env.WARNING_EMAIL_SMTP_PORT,
  45. secure: true, // Use true for port 465, false for port 587
  46. auth: {
  47. user: process.env.WARNING_EMAIL_SMTP_USERNAME,
  48. pass: process.env.WARNING_EMAIL_SMTP_PASSWORD,
  49. },
  50. });
  51. export default class Meteostanice {
  52. static add(owner, name, description) {
  53. const id = nanoid()
  54. const websocketKey = generateSecureRandomString()
  55. meteostaniceDB.prepare(`
  56. INSERT INTO list (id, owner, name, description, websocketKey)
  57. VALUES (?, ?, ?, ?, ?);
  58. `).run(id, owner, name, description, websocketKey)
  59. }
  60. static get(owner, id) {
  61. const statement = meteostaniceDB.prepare(`
  62. SELECT *
  63. FROM list
  64. WHERE id = $id
  65. AND (
  66. owner = $owner
  67. OR EXISTS (
  68. SELECT 1
  69. FROM json_each(list.subowners)
  70. WHERE value = $owner
  71. )
  72. );
  73. `);
  74. const result = statement.get({
  75. $owner: owner,
  76. $id: id
  77. });
  78. return result
  79. }
  80. static getById(id) {
  81. const statement = meteostaniceDB.prepare(`
  82. SELECT *
  83. FROM list
  84. WHERE id = $id
  85. `);
  86. const result = statement.get({
  87. $id: id
  88. });
  89. return result
  90. }
  91. static getWebsocket(key) {
  92. const statement = meteostaniceDB.prepare(`
  93. SELECT *
  94. FROM list
  95. WHERE websocketKey = $key;
  96. `)
  97. const result = statement.get({
  98. $key: key
  99. });
  100. return result
  101. }
  102. static getOwned(owner) {
  103. const statement = meteostaniceDB.prepare(`
  104. SELECT *
  105. FROM list
  106. WHERE owner = $owner
  107. OR EXISTS (
  108. SELECT *
  109. FROM json_each(list.subowners)
  110. WHERE value = $owner
  111. )
  112. ORDER BY timestamp DESC;
  113. `)
  114. const result = statement.all({
  115. $owner: owner
  116. });
  117. return result
  118. }
  119. static edit(id, newName, newDescription, newOwner, subowners) {
  120. let newSubowners = subowners ? subowners.split(',').map(s => s.trim()).filter(Boolean) : [];
  121. meteostaniceDB.prepare(`
  122. update list
  123. set name = ?,
  124. description = ?,
  125. owner = ?,
  126. subowners = json(?)
  127. where id = ?;
  128. `).run(newName, newDescription, newOwner, JSON.stringify(newSubowners), id)
  129. }
  130. static editOwnerOnOwned(owner, newOwner) {
  131. meteostaniceDB.prepare(`
  132. UPDATE list
  133. SET
  134. -- Update the primary owner if it matches
  135. owner = CASE WHEN owner = $owner THEN $newOwner ELSE owner END,
  136. -- Update the subowners array if it contains the owner
  137. subowners = CASE
  138. WHEN EXISTS (SELECT 1 FROM json_each(subowners) WHERE value = $owner)
  139. THEN (
  140. SELECT json_group_array(
  141. CASE WHEN value = $owner THEN $newOwner ELSE value END
  142. )
  143. FROM json_each(subowners)
  144. )
  145. ELSE subowners
  146. END
  147. WHERE owner = $owner
  148. OR EXISTS (SELECT 1 FROM json_each(subowners) WHERE value = $owner);
  149. `).run({
  150. $owner: owner,
  151. $newOwner: newOwner
  152. });
  153. }
  154. static delete(id) {
  155. meteostaniceDB.prepare(`
  156. DELETE
  157. FROM data
  158. WHERE meteostanica = $id;
  159. `).run({
  160. $id: id
  161. });
  162. meteostaniceDB.prepare(`
  163. DELETE
  164. FROM public
  165. WHERE id = $id;
  166. `).run({
  167. $id: id
  168. });
  169. meteostaniceDB.prepare(`
  170. DELETE
  171. FROM list
  172. WHERE id = $id;
  173. `).run({
  174. $id: id
  175. });
  176. }
  177. static deleteOwned(owner) {
  178. const meteostanice = this.getOwned(owner)
  179. for (const meteostanica of meteostanice) {
  180. this.delete(meteostanica.owner, meteostanica.id)
  181. }
  182. }
  183. static postData(meteostanica, indoorTemp, indoorPressure, indoorHumidity, outdoorConnected, outdoorTemp, outdoorPressure, outdoorHumidity, timestamp) {
  184. const id = nanoid();
  185. // 1. Define base columns and values
  186. let columns = ['id', 'meteostanica', 'indoorTemp', 'indoorPressure', 'indoorHumidity', 'outdoorConnected', 'outdoorTemp', 'outdoorPressure', 'outdoorHumidity'];
  187. let placeholders = ['?', '?', '?', '?', '?', '?', '?', '?', '?'];
  188. let args = [id, meteostanica, indoorTemp, indoorPressure, indoorHumidity, outdoorConnected, outdoorTemp, outdoorPressure, outdoorHumidity];
  189. // 2. Conditionally add timestamp
  190. if (timestamp) {
  191. columns.push('timestamp');
  192. placeholders.push('?');
  193. args.push(timestamp);
  194. }
  195. const statement = meteostaniceDB.prepare(`
  196. INSERT INTO data (${columns.join(', ')})
  197. VALUES (${placeholders.join(', ')})
  198. `);
  199. statement.run(...args);
  200. }
  201. static getData(meteostanica) {
  202. const statement = meteostaniceDB.prepare(`
  203. SELECT *
  204. FROM data
  205. WHERE meteostanica = $meteostanica
  206. ORDER BY timestamp DESC;
  207. `)
  208. const result = statement.all({
  209. $meteostanica: meteostanica
  210. });
  211. return result
  212. }
  213. static getDataProperty(meteostanica, property) {
  214. const tableNames = meteostaniceDB.prepare(`PRAGMA table_info('data');`).all()
  215. if (!tableNames.find(i => i.name === property)) return null
  216. const statement = meteostaniceDB.prepare(`
  217. SELECT ${property}
  218. FROM data
  219. WHERE meteostanica = $meteostanica
  220. ORDER BY timestamp DESC
  221. LIMIT 10;
  222. `)
  223. const result = statement.all({
  224. $meteostanica: meteostanica
  225. });
  226. return result
  227. }
  228. static getDataPropertyDaily(meteostanica, property, date) {
  229. const tableNames = meteostaniceDB.prepare(`PRAGMA table_info('data');`).all()
  230. if (!tableNames.find(i => i.name === property)) return null
  231. const statement = meteostaniceDB.prepare(`
  232. SELECT strftime('%Y-%m-%d %H:00:00', timestamp) AS timeMark,
  233. AVG(${property}) AS value
  234. FROM data
  235. WHERE meteostanica = ? AND date(timestamp) = ? -- Pass 'YYYY-MM-DD' here
  236. GROUP BY timeMark
  237. ORDER BY timeMark;
  238. `)
  239. const result = statement.all(meteostanica, date);
  240. return result
  241. }
  242. static getDataPropertyMonthly(meteostanica, property, yearMonth) {
  243. const tableNames = meteostaniceDB.prepare(`PRAGMA table_info('data');`).all()
  244. if (!tableNames.find(i => i.name === property)) return null
  245. const statement = meteostaniceDB.prepare(`
  246. SELECT date(timestamp) AS timeMark,
  247. AVG(${property}) AS value
  248. FROM data
  249. WHERE meteostanica = ? AND strftime('%Y-%m', timestamp) = ? -- Pass 'YYYY-MM' here
  250. GROUP BY timeMark
  251. ORDER BY timeMark;
  252. `)
  253. const result = statement.all(meteostanica, yearMonth);
  254. return result
  255. }
  256. static getDataPropertyYearly(meteostanica, property, year) {
  257. const tableNames = meteostaniceDB.prepare(`PRAGMA table_info('data');`).all()
  258. if (!tableNames.find(i => i.name === property)) return null
  259. const statement = meteostaniceDB.prepare(`
  260. SELECT strftime('%Y-%m', timestamp) AS timeMark,
  261. AVG(${property}) AS value
  262. FROM data
  263. WHERE meteostanica = ? AND strftime('%Y', timestamp) = ? -- Pass 'YYYY' here
  264. GROUP BY timeMark
  265. ORDER BY timeMark;
  266. `)
  267. const result = statement.all(meteostanica, year);
  268. return result
  269. }
  270. static getDataPropertyAllTime(meteostanica, property) {
  271. const tableNames = meteostaniceDB.prepare(`PRAGMA table_info('data');`).all()
  272. if (!tableNames.find(i => i.name === property)) return null
  273. const statement = meteostaniceDB.prepare(`
  274. SELECT strftime('%Y', timestamp) AS timeMark,
  275. AVG(${property}) AS value
  276. FROM data
  277. WHERE meteostanica = ?
  278. GROUP BY timeMark
  279. ORDER BY timeMark;
  280. `)
  281. const result = statement.all(meteostanica);
  282. return result
  283. }
  284. static getDateMap(meteostanica) {
  285. const statement = meteostaniceDB.query("SELECT DISTINCT date(timestamp) as d FROM data WHERE meteostanica = ? ORDER BY d ASC");
  286. const rows = statement.all(meteostanica);
  287. return rows.reduce((acc, row) => {
  288. const [year, month, day] = row.d.split("-");
  289. if (!acc[year]) acc[year] = {};
  290. if (!acc[year][month]) acc[year][month] = [];
  291. acc[year][month].push(day);
  292. return acc;
  293. }, {});
  294. }
  295. static resetWebsocketKey(id) {
  296. const websocketKey = generateSecureRandomString()
  297. meteostaniceDB.prepare(`
  298. update list
  299. set websocketKey = ?
  300. where id = ?;
  301. `).run(websocketKey, id)
  302. }
  303. static getEmails(id) {
  304. const statement = meteostaniceDB.prepare(`
  305. WITH all_emails AS (
  306. -- Get the primary owner
  307. SELECT owner AS email
  308. FROM list
  309. WHERE id = $id
  310. UNION
  311. -- Get all subowners from the JSON array
  312. SELECT json_each.value AS email
  313. FROM list, json_each(list.subowners)
  314. WHERE list.id = $id
  315. )
  316. SELECT email FROM all_emails WHERE email IS NOT NULL;
  317. `);
  318. // .all() returns an array of objects: [{email: '...'}, {email: '...'}]
  319. const rows = statement.all({ $id: id });
  320. // Map it to a simple array of strings: ['owner@email.com', 'subowner@email.com']
  321. return rows.map(row => row.email);
  322. }
  323. static getStationDataLast5Minutes(id) {
  324. const rows = meteostaniceDB.prepare(`
  325. SELECT * FROM data
  326. WHERE meteostanica = ?
  327. AND timestamp >= datetime('now', '-6 minutes')
  328. ORDER BY timestamp DESC
  329. LIMIT 5;
  330. `).all(id);
  331. // Only return the data if we have a full 5-minute window
  332. return rows.length >= 5 ? rows : null;
  333. }
  334. static editWarnings(id, warnings) {
  335. meteostaniceDB.prepare(`
  336. UPDATE list
  337. SET warnings = json(?)
  338. WHERE id = ?;
  339. `).run(JSON.stringify(warnings), id)
  340. }
  341. static async sendWarnings(email, subject, text, html) {
  342. return await transporter.sendMail({
  343. from: process.env.WARNING_EMAIL_SMTP_FROM,
  344. to: email,
  345. subject,
  346. text, // Plain-text version of the message
  347. html, // HTML version of the message
  348. });
  349. }
  350. }