From bc715b93f4f076deeae14dcda5f580f4d5df736d Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Sun, 19 Apr 2020 00:31:58 -0700 Subject: [PATCH 1/5] Checking in --- __mocks__/dgram.js | 5 ++++- __mocks__/pouchdb.js | 5 +++++ grill-polling.js | 17 ++++++++++------- grill-polling.spec.js | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 __mocks__/pouchdb.js create mode 100644 grill-polling.spec.js diff --git a/__mocks__/dgram.js b/__mocks__/dgram.js index 376737c..ad4c2e7 100644 --- a/__mocks__/dgram.js +++ b/__mocks__/dgram.js @@ -1 +1,4 @@ -module.exports = { createSocket: jest.fn() }; +import { EventEmitter } from 'events'; + + +module.exports = { createSocket: new EventEmitter() }; diff --git a/__mocks__/pouchdb.js b/__mocks__/pouchdb.js new file mode 100644 index 0000000..8176460 --- /dev/null +++ b/__mocks__/pouchdb.js @@ -0,0 +1,5 @@ +module.exports = jest.fn(() => ({ + put: () => { + return new Promise(() => true) + }, +})); diff --git a/grill-polling.js b/grill-polling.js index edb0fb1..84ba2bf 100644 --- a/grill-polling.js +++ b/grill-polling.js @@ -7,7 +7,7 @@ import * as store from "./datastore-pouch"; const socket = dgram.createSocket("udp4"); -let status = new GrillStatus(Buffer.from('')); +let status = new GrillStatus(Buffer.from("")); const renderChart = async () => { const history = await store.all(); @@ -27,17 +27,19 @@ socket.on("message", async (msg, info) => { // console.log(info); // console.log('end message ===='); // @todo check for non grill status messages - status = new GrillStatus(Buffer.from(msg)); + status = { + timestamp: new Date().getTime(), + ...new GrillStatus(Buffer.from(msg)), + }; // console.log('hex ', status.hex); // console.log('grillOptions', ' ', status.settings); // console.log(' '); // console.log('grilOptions decode', status.grillOptions); if (status.currentGrillTemp !== NaN) { // assume valid status - const dataToStore = { timestamp: new Date().getTime(), ...status }; - await store.add({ timestamp: new Date().getTime(), ...status }); - console.clear(); - console.log(dataToStore); + await store.add(status); + // console.clear(); + // console.log(dataToStore); } }); @@ -62,7 +64,8 @@ store.setup(); // @todo change to exports module.exports = { + socket, pollStatus, sendOnce, - latestStatus + latestStatus, }; diff --git a/grill-polling.spec.js b/grill-polling.spec.js new file mode 100644 index 0000000..53f38f4 --- /dev/null +++ b/grill-polling.spec.js @@ -0,0 +1,32 @@ +import { socket, latestStatus } from "./grill-polling"; +import GrillStatus from "./GrillStatus"; +import * as store from "./datastore-pouch"; + +jest.spyOn(store, 'add'); + +jest.mock("./GrillStatus", () => { + return jest.fn(() => ({ + state: 'on', + })); +}); + +describe.only("grill polling", () => { + it("calls store.add on message emit", async (done) => { + socket.emit("message", 'cats'); + expect(GrillStatus).toHaveBeenCalled(); + expect(latestStatus().state).toEqual('on'); + expect(latestStatus().timestamp).toBeDefined(); + expect(store.add).toHaveBeenCalledWith(latestStatus()); + done(); + }); + it("calls store.addEvent when on/off changes", async (done) => { + socket.emit("message", 'cats'); + expect(latestStatus().state).toEqual('on'); + GrillStatus.mockReturnValueOnce({ + state: 'off', + }); + socket.emit("message", 'cats'); + expect(latestStatus().state).toEqual('off'); + done(); + }); +}); From 330a7bdd42974e967ab5a74650c79924338815d8 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Sun, 19 Apr 2020 01:08:32 -0700 Subject: [PATCH 2/5] checking in --- __mocks__/pouchdb.js | 3 +++ datastore-pouch.js | 5 ++++- grill-polling.js | 6 +++++- grill-polling.spec.js | 14 +++++++------- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/__mocks__/pouchdb.js b/__mocks__/pouchdb.js index 8176460..672e1b1 100644 --- a/__mocks__/pouchdb.js +++ b/__mocks__/pouchdb.js @@ -2,4 +2,7 @@ module.exports = jest.fn(() => ({ put: () => { return new Promise(() => true) }, + allDocs: () => { + return new Promise(() => true) + }, })); diff --git a/datastore-pouch.js b/datastore-pouch.js index 63f1186..de067a5 100644 --- a/datastore-pouch.js +++ b/datastore-pouch.js @@ -19,6 +19,9 @@ export async function all() { .then((data) => data.rows.map(({ doc }) => doc).reverse()); } +export async function addEvent() { + return await db.allDocs({ include_docs: true, limit: 1, descending: true }); +} /** * const PouchDB = require("pouchdb"); @@ -44,4 +47,4 @@ export async function all() { .catch(err => console.error(err)); } - */ \ No newline at end of file + */ diff --git a/grill-polling.js b/grill-polling.js index 84ba2bf..8d2ef62 100644 --- a/grill-polling.js +++ b/grill-polling.js @@ -27,9 +27,13 @@ socket.on("message", async (msg, info) => { // console.log(info); // console.log('end message ===='); // @todo check for non grill status messages + const grillStatus = new GrillStatus(Buffer.from(msg)); + if (grillStatus.state !== status.state) { + await store.addEvent({}); + } status = { timestamp: new Date().getTime(), - ...new GrillStatus(Buffer.from(msg)), + ...grillStatus, }; // console.log('hex ', status.hex); // console.log('grillOptions', ' ', status.settings); diff --git a/grill-polling.spec.js b/grill-polling.spec.js index 53f38f4..329ab1e 100644 --- a/grill-polling.spec.js +++ b/grill-polling.spec.js @@ -3,6 +3,7 @@ import GrillStatus from "./GrillStatus"; import * as store from "./datastore-pouch"; jest.spyOn(store, 'add'); +jest.spyOn(store, 'addEvent'); jest.mock("./GrillStatus", () => { return jest.fn(() => ({ @@ -14,19 +15,18 @@ describe.only("grill polling", () => { it("calls store.add on message emit", async (done) => { socket.emit("message", 'cats'); expect(GrillStatus).toHaveBeenCalled(); - expect(latestStatus().state).toEqual('on'); + latestStatus().state = 'beans'; + expect(latestStatus().state).toEqual('beans'); expect(latestStatus().timestamp).toBeDefined(); expect(store.add).toHaveBeenCalledWith(latestStatus()); done(); }); it("calls store.addEvent when on/off changes", async (done) => { - socket.emit("message", 'cats'); - expect(latestStatus().state).toEqual('on'); - GrillStatus.mockReturnValueOnce({ - state: 'off', - }); - socket.emit("message", 'cats'); + latestStatus().state = 'off'; expect(latestStatus().state).toEqual('off'); + latestStatus().state = 'on'; + expect(latestStatus().state).toEqual('on'); + expect(store.addEvent).toHaveBeenCalled(); done(); }); }); From c34cb69348443dd6a76cb5aaa543e729661e5f8d Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Sun, 19 Apr 2020 12:00:26 -0700 Subject: [PATCH 3/5] Re-org of project --- README.md | 26 +++++++ README_database.md | 22 ------ {routes => api}/index.js | 0 {routes => api}/power.js | 0 {routes => api}/settings.js | 9 ++- {routes => api}/temp.js | 0 app.js | 2 +- config.js => config/index.js | 1 - constants/csv.js | 13 ++++ constants/emitter-events.js | 2 + constants.js => constants/grill-commands.js | 41 +++-------- constants/grill-events.js | 4 ++ constants/grill.js | 10 +++ constants/index.js | 12 ++++ data/brisket2.csv | 9 --- datastore-pouch.js => datastore/pouch.js | 2 - grill-polling.js | 75 -------------------- grill-polling.spec.js | 32 --------- index.js | 4 +- index.spec.js | 36 +++++----- kittens/000005.ldb | Bin 237 -> 0 bytes kittens/000008.ldb | Bin 645 -> 0 bytes kittens/000011.ldb | Bin 287 -> 0 bytes kittens/000012.log | Bin 178 -> 0 bytes kittens/CURRENT | 1 - kittens/LOCK | 0 kittens/LOG | 5 -- kittens/LOG.old | 5 -- kittens/MANIFEST-000010 | Bin 280 -> 0 bytes middleware/sendCommand.js | 6 +- GrillStatus.js => models/GrillStatus.js | 22 +++--- package-lock.json | 13 ++++ package.json | 1 + sandbox/basic.js | 46 ------------ services/eventEmitter.js | 2 + services/grillOnError.js | 5 ++ services/grillOnMessage.js | 31 ++++++++ services/grillPollStatus.js | 11 +++ services/grillSendCommandOnce.js | 8 +++ services/grillSocket.js | 2 + services/observableGrillStatus.js | 36 ++++++++++ services/observableGrillStatus.spec.js | 27 +++++++ services/renderDebugChart.js | 9 +++ 43 files changed, 262 insertions(+), 268 deletions(-) delete mode 100644 README_database.md rename {routes => api}/index.js (100%) rename {routes => api}/power.js (100%) rename {routes => api}/settings.js (66%) rename {routes => api}/temp.js (100%) rename config.js => config/index.js (67%) create mode 100644 constants/csv.js create mode 100644 constants/emitter-events.js rename constants.js => constants/grill-commands.js (59%) create mode 100644 constants/grill-events.js create mode 100644 constants/grill.js create mode 100644 constants/index.js delete mode 100644 data/brisket2.csv rename datastore-pouch.js => datastore/pouch.js (97%) delete mode 100644 grill-polling.js delete mode 100644 grill-polling.spec.js delete mode 100644 kittens/000005.ldb delete mode 100644 kittens/000008.ldb delete mode 100644 kittens/000011.ldb delete mode 100644 kittens/000012.log delete mode 100644 kittens/CURRENT delete mode 100644 kittens/LOCK delete mode 100644 kittens/LOG delete mode 100644 kittens/LOG.old delete mode 100644 kittens/MANIFEST-000010 rename GrillStatus.js => models/GrillStatus.js (91%) delete mode 100644 sandbox/basic.js create mode 100644 services/eventEmitter.js create mode 100644 services/grillOnError.js create mode 100644 services/grillOnMessage.js create mode 100644 services/grillPollStatus.js create mode 100644 services/grillSendCommandOnce.js create mode 100644 services/grillSocket.js create mode 100644 services/observableGrillStatus.js create mode 100644 services/observableGrillStatus.spec.js create mode 100644 services/renderDebugChart.js diff --git a/README.md b/README.md index ad19ec2..126aa60 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ ## TODO update with latest +## Structure + +This project is based on structure in this article: https://softwareontheroad.com/ideal-nodejs-project-structure/. A quick read over will help you better navigate this project. + + ## Command API Command api allows for remote control of your GMG. A few implementation notes @@ -123,3 +128,24 @@ rvictl -x ## GET /cook // returns all cooks + + +## OLD database notes + +- `brew install rethinkdb` +- `rethinkdb` +- in browser go to http://localhost:8080/ + +https://marmelab.com/blog/2019/09/25/couchdb_pouchdb_serious_firebase_alternative.html + + +Good article on optimization +https://www.mongodb.com/blog/post/time-series-data-and-mongodb-part-2-schema-design-best-practices + + +Filtering changes +https://pouchdb.com/api.html#filtered-changes + + +Better doc IDS that have our sensor prop names in them? +https://pouchdb.com/2014/06/17/12-pro-tips-for-better-code-with-pouchdb.html \ No newline at end of file diff --git a/README_database.md b/README_database.md deleted file mode 100644 index 5264af0..0000000 --- a/README_database.md +++ /dev/null @@ -1,22 +0,0 @@ - -# To get running - -- `brew install rethinkdb` -- `rethinkdb` -- in browser go to http://localhost:8080/ - - - -https://marmelab.com/blog/2019/09/25/couchdb_pouchdb_serious_firebase_alternative.html - - -Good article on optimization -https://www.mongodb.com/blog/post/time-series-data-and-mongodb-part-2-schema-design-best-practices - - -Filtering changes -https://pouchdb.com/api.html#filtered-changes - - -Better doc IDS that have our sensor prop names in them? -https://pouchdb.com/2014/06/17/12-pro-tips-for-better-code-with-pouchdb.html \ No newline at end of file diff --git a/routes/index.js b/api/index.js similarity index 100% rename from routes/index.js rename to api/index.js diff --git a/routes/power.js b/api/power.js similarity index 100% rename from routes/power.js rename to api/power.js diff --git a/routes/settings.js b/api/settings.js similarity index 66% rename from routes/settings.js rename to api/settings.js index 254787f..d322d1e 100644 --- a/routes/settings.js +++ b/api/settings.js @@ -3,14 +3,17 @@ import requireCode from "../middleware/requireCode"; import sendCommand from "../middleware/sendCommand"; import jsonCommandSuccessMsg from "../middleware/jsonCommandSuccessMsg"; import { HEX_COMMANDS } from "../constants"; -import { latestStatus } from "../grill-polling"; +import observableGrillStatus from "./../services/observableGrillStatus"; const router = express.Router(); router.get( "/pizza", requireCode, - sendCommand(() => HEX_COMMANDS.setPizzaMode(latestStatus().settings), "hex"), + sendCommand( + () => HEX_COMMANDS.setPizzaMode(observableGrillStatus.value.settings), + "hex" + ), jsonCommandSuccessMsg("pizza mode") ); @@ -18,7 +21,7 @@ router.get( "/regular", requireCode, sendCommand( - () => HEX_COMMANDS.setRegularMode(latestStatus().settings), + () => HEX_COMMANDS.setRegularMode(observableGrillStatus.value.settings), "hex" ), jsonCommandSuccessMsg("regular mode") diff --git a/routes/temp.js b/api/temp.js similarity index 100% rename from routes/temp.js rename to api/temp.js diff --git a/app.js b/app.js index 1f96736..d47368a 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,6 @@ import express from "express"; -import { temp, power, settings } from "./routes"; +import { temp, power, settings } from "./api"; const app = express(); diff --git a/config.js b/config/index.js similarity index 67% rename from config.js rename to config/index.js index 63561d5..862b060 100644 --- a/config.js +++ b/config/index.js @@ -1,4 +1,3 @@ -export const FILE = "./data/brisket2.csv"; export const IP = "192.168.86.134"; export const PORT = 8080; export const INTERVAL = 1000; \ No newline at end of file diff --git a/constants/csv.js b/constants/csv.js new file mode 100644 index 0000000..0318770 --- /dev/null +++ b/constants/csv.js @@ -0,0 +1,13 @@ +export const HEADERS = [ + "timestamp", + "state", + "isOn", + "currentGrillTemp", + "desiredGrillTemp", + "currentProbe1Temp", + "desiredProbe1Temp", + "currentProbe2Temp", + "desiredProbe2Temp", + "fanModeActive", + "lowPelletAlarmActive", +]; diff --git a/constants/emitter-events.js b/constants/emitter-events.js new file mode 100644 index 0000000..4300d27 --- /dev/null +++ b/constants/emitter-events.js @@ -0,0 +1,2 @@ +// @todo remove, not needed? +export const NEW_GRILL_EVENT = 'NEW_GRILL_EVENT'; \ No newline at end of file diff --git a/constants.js b/constants/grill-commands.js similarity index 59% rename from constants.js rename to constants/grill-commands.js index 461a9b7..52c6eea 100644 --- a/constants.js +++ b/constants/grill-commands.js @@ -1,16 +1,10 @@ -export const HEADERS = [ - "timestamp", - "state", - "isOn", - "currentGrillTemp", - "desiredGrillTemp", - "currentProbe1Temp", - "desiredProbe1Temp", - "currentProbe2Temp", - "desiredProbe2Temp", - "fanModeActive", - "lowPelletAlarmActive", -]; +String.prototype.replaceAt = function (index, replacement) { + return ( + this.substr(0, index) + + replacement + + this.substr(index + replacement.length) + ); +}; export const COMMANDS = Object.freeze({ powerOn: "UK001!", @@ -25,30 +19,13 @@ export const COMMANDS = Object.freeze({ // setRegularMode: 'UC.+ 9 !' }); -String.prototype.replaceAt = function (index, replacement) { - return ( - this.substr(0, index) + - replacement + - this.substr(index + replacement.length) - ); -}; - export const HEX_COMMANDS = Object.freeze({ setPizzaMode: (settings) => { - const mode = `55430${settings.replaceAt(1, '2')}21`; + const mode = `55430${settings.replaceAt(1, "2")}21`; return mode; }, setRegularMode: (settings) => { - const mode = `55430${settings.replaceAt(1, '0')}21`; + const mode = `55430${settings.replaceAt(1, "0")}21`; return mode; }, }); - -// UN - what does this do? -// UF150 -// Uf150 - - -// used to work -// 5543052b023220202020 -// 52b14321919191921 \ No newline at end of file diff --git a/constants/grill-events.js b/constants/grill-events.js new file mode 100644 index 0000000..09e68af --- /dev/null +++ b/constants/grill-events.js @@ -0,0 +1,4 @@ +export const POWER_ON = "POWER_ON"; +export const POWER_ON_COLDSMOKE = "POWER_ON_COLDSMOKE"; +export const FAN_MODE = "FAN_MODE"; +export const POWER_OFF = "POWER_OFF"; diff --git a/constants/grill.js b/constants/grill.js new file mode 100644 index 0000000..f80fa43 --- /dev/null +++ b/constants/grill.js @@ -0,0 +1,10 @@ +export const GRILL_NOT_SURE_MAP = { + 0: "Climate ICY", + 4: "Climate COLD", + 8: "Climate AVERAGE", + 12: "Climate WARM", + 1: "Climate HOT", + 10: "Lock temp display OFF", + 9: "Auto Revert Wifi OFF", + 11: "Auto Revert Wifi ON", +}; diff --git a/constants/index.js b/constants/index.js new file mode 100644 index 0000000..7c0697d --- /dev/null +++ b/constants/index.js @@ -0,0 +1,12 @@ +import { COMMANDS, HEX_COMMANDS } from "./grill-commands"; +import * as GRILL_EVENTS from "./grill-events"; +import * as EMITTER_EVENTS from "./emitter-events"; +import { GRILL_NOT_SURE_MAP } from "./grill"; + +export { + COMMANDS, + HEX_COMMANDS, + GRILL_EVENTS, + GRILL_NOT_SURE_MAP, + EMITTER_EVENTS, +}; diff --git a/data/brisket2.csv b/data/brisket2.csv deleted file mode 100644 index c3d9ce7..0000000 --- a/data/brisket2.csv +++ /dev/null @@ -1,9 +0,0 @@ -timestamp,state,isOn,currentGrillTemp,desiredGrillTemp,currentProbe1Temp,desiredProbe1Temp,currentProbe2Temp,desiredProbe2Temp,fanModeActive,lowPelletAlarmActive -1586196504422,off,false,47,0,48,0,49,0,false,false -1586196506410,off,false,47,0,48,0,49,0,false,false -1586196508408,off,false,47,0,48,0,49,0,false,false -1586196510407,off,false,47,0,48,0,49,0,false,false -1586196512415,off,false,47,0,48,0,49,0,false,false -1586196514416,off,false,47,0,48,0,49,0,false,false -1586217994172,off,false,57,0,60,0,60,0,false,false -1586217996162,off,false,57,0,60,0,60,0,false,false diff --git a/datastore-pouch.js b/datastore/pouch.js similarity index 97% rename from datastore-pouch.js rename to datastore/pouch.js index de067a5..2512a71 100644 --- a/datastore-pouch.js +++ b/datastore/pouch.js @@ -3,8 +3,6 @@ const uuid = require("uuid/v4"); const db = new PouchDB("http://localhost:5984/api_test_1"); -export async function setup() {} - export async function add(item) { return await db .put({ _id: `${item.timestamp}`, ...item }) diff --git a/grill-polling.js b/grill-polling.js deleted file mode 100644 index 8d2ef62..0000000 --- a/grill-polling.js +++ /dev/null @@ -1,75 +0,0 @@ -const dgram = require("dgram"); -const GrillStatus = require("./GrillStatus"); -const asciichart = require("asciichart"); -import { PORT, IP, INTERVAL } from "./config"; -import { COMMANDS } from "./constants"; -import * as store from "./datastore-pouch"; - -const socket = dgram.createSocket("udp4"); - -let status = new GrillStatus(Buffer.from("")); - -const renderChart = async () => { - const history = await store.all(); - const data = history.map(({ currentProbe1Temp }) => currentProbe1Temp); - console.clear(); - console.log(asciichart.plot(data, { height: 6 })); -}; - -socket.on("error", (err) => { - console.error("error", err); -}); - -socket.on("message", async (msg, info) => { - // console.log('message ===='); - // console.log(msg); - // console.log(Buffer.from(msg).toString()); - // console.log(info); - // console.log('end message ===='); - // @todo check for non grill status messages - const grillStatus = new GrillStatus(Buffer.from(msg)); - if (grillStatus.state !== status.state) { - await store.addEvent({}); - } - status = { - timestamp: new Date().getTime(), - ...grillStatus, - }; - // console.log('hex ', status.hex); - // console.log('grillOptions', ' ', status.settings); - // console.log(' '); - // console.log('grilOptions decode', status.grillOptions); - if (status.currentGrillTemp !== NaN) { - // assume valid status - await store.add(status); - // console.clear(); - // console.log(dataToStore); - } -}); - -const latestStatus = () => { - return status; -}; - -const sendOnce = (message, mode = "ascii") => { - const data = Buffer.from(message, mode); - socket.send(data, 0, data.byteLength, PORT, IP, (error) => {}); -}; - -const pollStatus = () => { - const data = Buffer.from(COMMANDS.getGrillStatus, "ascii"); - socket.send(data, 0, data.byteLength, PORT, IP, (error) => {}); - setTimeout(() => { - pollStatus(); - }, INTERVAL); -}; - -store.setup(); - -// @todo change to exports -module.exports = { - socket, - pollStatus, - sendOnce, - latestStatus, -}; diff --git a/grill-polling.spec.js b/grill-polling.spec.js deleted file mode 100644 index 329ab1e..0000000 --- a/grill-polling.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import { socket, latestStatus } from "./grill-polling"; -import GrillStatus from "./GrillStatus"; -import * as store from "./datastore-pouch"; - -jest.spyOn(store, 'add'); -jest.spyOn(store, 'addEvent'); - -jest.mock("./GrillStatus", () => { - return jest.fn(() => ({ - state: 'on', - })); -}); - -describe.only("grill polling", () => { - it("calls store.add on message emit", async (done) => { - socket.emit("message", 'cats'); - expect(GrillStatus).toHaveBeenCalled(); - latestStatus().state = 'beans'; - expect(latestStatus().state).toEqual('beans'); - expect(latestStatus().timestamp).toBeDefined(); - expect(store.add).toHaveBeenCalledWith(latestStatus()); - done(); - }); - it("calls store.addEvent when on/off changes", async (done) => { - latestStatus().state = 'off'; - expect(latestStatus().state).toEqual('off'); - latestStatus().state = 'on'; - expect(latestStatus().state).toEqual('on'); - expect(store.addEvent).toHaveBeenCalled(); - done(); - }); -}); diff --git a/index.js b/index.js index 8b76000..fff83e7 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,8 @@ -import { pollStatus } from "./grill-polling"; +// import { pollStatus } from "./grill-polling"; const app = require("./app.js"); -pollStatus(); +// @TODO start polling here app.listen(3000, function () { console.log("Example app listening on port 3000!"); diff --git a/index.spec.js b/index.spec.js index eeb2816..215c6f1 100644 --- a/index.spec.js +++ b/index.spec.js @@ -1,17 +1,14 @@ import { COMMANDS, HEX_COMMANDS } from "./constants"; import * as codeGen from "./helpers/code-gen"; import url from "url"; +import { grillSendCommandOnce } from "./services/grillSendCommandOnce"; +import observableGrillStatus from "./services/observableGrillStatus"; const request = require("supertest"); const app = require("./app"); -const grillPolling = require("./grill-polling"); -jest.mock("./grill-polling", () => ({ - pollStatus: jest.fn(), - sendOnce: jest.fn(), - latestStatus: jest.fn(() => ({ - settings: "xxxxxxxxxxxx", - })), +jest.mock("./services/grillSendCommandOnce", () => ({ + grillSendCommandOnce: jest.fn(), })); jest.spyOn(codeGen, "newCode"); @@ -21,7 +18,7 @@ describe("COMMANDS", () => { codeGen.newCode.mockClear(); }); afterEach(() => { - grillPolling.sendOnce.mockClear(); + grillSendCommandOnce.mockClear(); }); describe("Command API", () => { describe("Power commands", () => { @@ -51,7 +48,10 @@ describe("COMMANDS", () => { expect(link).toEqual(expectedLink); const res2 = await request(app).get(`/command/power/off?code=${code}`); expect(res2.statusCode).toEqual(200); - expect(grillPolling.sendOnce).toHaveBeenCalledWith(COMMANDS.powerOff, undefined); + expect(grillSendCommandOnce).toHaveBeenCalledWith( + COMMANDS.powerOff, + undefined + ); expect(codeGen.newCode).toHaveBeenCalledTimes(2); done(); }); @@ -60,7 +60,7 @@ describe("COMMANDS", () => { expect(res.statusCode).toEqual(403); expect(res.body.message).toEqual("invalid code"); expect(codeGen.newCode).toHaveBeenCalledTimes(1); - expect(grillPolling.sendOnce).not.toHaveBeenCalled(); + expect(grillSendCommandOnce).not.toHaveBeenCalled(); done(); }); describe("quick check for each", () => { @@ -101,10 +101,12 @@ describe("COMMANDS", () => { }, }); expect(link).toEqual(expectedLink); - const res2 = await request(app).get(`/command/temp/grill?temp=150&code=${code}`); + const res2 = await request(app).get( + `/command/temp/grill?temp=150&code=${code}` + ); expect(res2.statusCode).toEqual(200); expect(res2.body.message).toEqual("Sent grill grill temp 150 command"); - expect(grillPolling.sendOnce).toHaveBeenCalledWith( + expect(grillSendCommandOnce).toHaveBeenCalledWith( COMMANDS.setGrillTempF(150), undefined ); @@ -142,11 +144,13 @@ describe("COMMANDS", () => { }, }); expect(link).toEqual(expectedLink); - const res2 = await request(app).get(`/command/settings/pizza?code=${code}`); + const res2 = await request(app).get( + `/command/settings/pizza?code=${code}` + ); expect(res2.statusCode).toEqual(200); expect(res2.body.message).toEqual("Sent grill pizza mode command"); - expect(grillPolling.sendOnce).toHaveBeenCalledWith( - HEX_COMMANDS.setPizzaMode(grillPolling.latestStatus().settings), + expect(grillSendCommandOnce).toHaveBeenCalledWith( + HEX_COMMANDS.setPizzaMode(observableGrillStatus.value.settings), "hex" ); expect(codeGen.newCode).toHaveBeenCalledTimes(2); @@ -171,7 +175,7 @@ describe("COMMANDS", () => { // it("on", async (done) => { // const res = await request(app).get("/command/on"); // expect(res.statusCode).toEqual(200); - // expect(grillPolling.sendOnce).toHaveBeenCalledWith(COMMANDS.powerOn); + // expect(grillSendCommandOnce).toHaveBeenCalledWith(COMMANDS.powerOn); // done(); // }); }); diff --git a/kittens/000005.ldb b/kittens/000005.ldb deleted file mode 100644 index 1da02f85c06d78dd425e7e1d9f5efcd07337c872..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 237 zcmZQjR6D#sH?<^Dx40y~DE09E_?-OY#GLrj(##Y_Mg}lYGD|c{G&L|v(KSdkOw%|_#?NtaRz zi+B?}sYelX@eg=VFMFtX@hl$nDvBU@UJp8P!J`k};eFq{_rCYdu67MA&G2l`8=D!u zlT+c-*`DQKRI~9M((ulN2#6@@lIu)LDl%e_*!S}UB#VWDx3u~zg~6tJvtp28?0Jl4 zq}wM6hcOeI`Zkk8-2=`uJr4CTq}b~DL&yq_Lq=0QiJ6cz#csmmc#DZ8eMmV6-OnW@ z1B19MP@5u6;KiR8-IR`0Wt-6ItOaWwuidm(Ez7Zd8bIKMt9Gl^v7L56rlp04r6Bh6 zhzew)GCnSlLaN8g(&_3Qxku)YGlOS`25*PvF z;BiW(Mt?Av4h($z@f)O_n|C8fNO$t!JF>d=)F9hT2zA?yCkwb|kbrV3snQ<;&!|C^ z%XnkH)W8vyU^g$hJobURN~Y__xsbSg_I%GiS;xv6>E=lQlDb7x<@l-7wZ%MV0WMXK zy(A7ap=~t-(*ip*TaLeC0)>w0x4h8yJC1F8K#&St*_b6%YUinkMfD4tC)p0YF{s)YWXexeGuqd`5*WG Z^ZvVg73+1SUO@kko4ANr_4zwTvJl<^J({hJ}0v912qv*gygxW4RfuHW@WAC^4sH=9Hus>E)!BrRJn0 z={c1G`I&i6Ir;gyZeS^+c19tPF0fhg@4nAu;AB3+_#Xlon!v(f;+fIs^Ya)DE;EXQ Ql;a2CzZ<$$O5JY*04@wjg8%>k diff --git a/kittens/000012.log b/kittens/000012.log deleted file mode 100644 index c1e8c50149e4d705dac22e6ff859602caef44556..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178 zcmd-(w0({r10xp$1TX?AM#;nbb5lzab&E^#i&78okI%_ZPRxlfEzL|(Q!-05N;EYv zO3^h)G)&VqF-tbqO-xO+)J--?N;6EhG&3|zN>uvtO0GPNfsq?(C=fcT9A@hk{Vx}TF7V!0ALt7h5!Hn diff --git a/kittens/CURRENT b/kittens/CURRENT deleted file mode 100644 index 3051f81..0000000 --- a/kittens/CURRENT +++ /dev/null @@ -1 +0,0 @@ -MANIFEST-000010 diff --git a/kittens/LOCK b/kittens/LOCK deleted file mode 100644 index e69de29..0000000 diff --git a/kittens/LOG b/kittens/LOG deleted file mode 100644 index 5ad124c..0000000 --- a/kittens/LOG +++ /dev/null @@ -1,5 +0,0 @@ -2020/04/06-12:14:43.178332 70000af6a000 Recovering log #9 -2020/04/06-12:14:43.178976 70000af6a000 Level-0 table #11: started -2020/04/06-12:14:43.180203 70000af6a000 Level-0 table #11: 287 bytes OK -2020/04/06-12:14:43.181677 70000af6a000 Delete type=3 #7 -2020/04/06-12:14:43.181938 70000af6a000 Delete type=0 #9 diff --git a/kittens/LOG.old b/kittens/LOG.old deleted file mode 100644 index 07ef927..0000000 --- a/kittens/LOG.old +++ /dev/null @@ -1,5 +0,0 @@ -2020/04/06-12:14:29.629341 700009e4a000 Recovering log #6 -2020/04/06-12:14:29.629967 700009e4a000 Level-0 table #8: started -2020/04/06-12:14:29.631150 700009e4a000 Level-0 table #8: 645 bytes OK -2020/04/06-12:14:29.632380 700009e4a000 Delete type=0 #6 -2020/04/06-12:14:29.632532 700009e4a000 Delete type=3 #4 diff --git a/kittens/MANIFEST-000010 b/kittens/MANIFEST-000010 deleted file mode 100644 index e7b260bb00db099018353f2399340ca75aad093f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 280 zcmXSKS;4i1fss)vC$%g!CnZVGsj?)sJhM2}IX|}`u_&=5zlfcIqm@b&cHpNNe!#@Df!9q$@!&uC5#*d4QGd_004*~Sb_im diff --git a/middleware/sendCommand.js b/middleware/sendCommand.js index 9cdc01b..c86e3d0 100644 --- a/middleware/sendCommand.js +++ b/middleware/sendCommand.js @@ -1,12 +1,12 @@ import { isFunction } from "lodash"; -import { sendOnce } from "../grill-polling"; +import { grillSendCommandOnce } from "../services/grillSendCommandOnce"; export default function (command, mode) { return function (req, res, next) { if (isFunction(command)) { - sendOnce(command(req, res), mode); + grillSendCommandOnce(command(req, res), mode); } else { - sendOnce(command, mode); + grillSendCommandOnce(command, mode); } next(); }; diff --git a/GrillStatus.js b/models/GrillStatus.js similarity index 91% rename from GrillStatus.js rename to models/GrillStatus.js index 6f5bbe0..c3f385a 100644 --- a/GrillStatus.js +++ b/models/GrillStatus.js @@ -1,3 +1,9 @@ +// @TODO unit test and simplify using `test` function +// @TODO use constants for power states, modes +// @TODO settings should be default string + +import { GRILL_NOT_SURE_MAP } from "../constants"; + const getRawValue = (hex, position) => { const value = hex.substr(position, 2); const parsed = parseInt(value, 16); @@ -63,17 +69,6 @@ const parseHex = (str) => { return parseInt(str, 16); }; -const someMap = { - 0: "Climate ICY", - 4: "Climate COLD", - 8: "Climate AVERAGE", - 12: "Climate WARM", - 1: "Climate HOT", - 10: "Lock temp display OFF", - 9: "Auto Revert Wifi OFF", - 11: "Auto Revert Wifi ON", -}; - class GrillStatus { constructor(bytes) { const hex = Buffer.from(bytes).toString("hex"); @@ -83,7 +78,8 @@ class GrillStatus { who: this.settings.substring(0, 1), pizzaMode: this.settings.substring(1, 2) === "2", manySettingsValue: parseHex(this.settings.substring(2, 3)), - lastManySettings: someMap[parseHex(this.settings.substring(2, 3))], + lastManySettings: + GRILL_NOT_SURE_MAP[parseHex(this.settings.substring(2, 3))], grillAdjustA: parseHex(this.settings.substring(3, 5)), // -20 to 20 range grillAdjustB: parseHex(this.settings.substring(5, 7)), probe1AdjustA: parseHex(this.settings.substring(7, 9)), // -25 to 25 deg range @@ -97,7 +93,7 @@ class GrillStatus { parseHex(this.settings.substring(2, 3)) === 0 ) { this.grillOptions.pizzaMode = false; - this.grillOptions.lastManySettings = someMap[1]; + this.grillOptions.lastManySettings = GRILL_NOT_SURE_MAP[1]; } this.hex = hex; this.isOn = this.state === "on" || this.state === "cold smoke mode"; diff --git a/package-lock.json b/package-lock.json index 8bee40f..3086d19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7830,6 +7830,14 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", "dev": true }, + "rxjs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "requires": { + "tslib": "^1.9.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -8747,6 +8755,11 @@ "punycode": "^2.1.0" } }, + "tslib": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index 9677a28..53ece28 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "pouchdb-find": "^7.2.1", "random-words": "^1.1.1", "rethinkdb": "^2.4.2", + "rxjs": "^6.5.5", "short-uuid": "^3.1.1", "uuid": "^7.0.3" }, diff --git a/sandbox/basic.js b/sandbox/basic.js deleted file mode 100644 index a1878c5..0000000 --- a/sandbox/basic.js +++ /dev/null @@ -1,46 +0,0 @@ -import express from 'express'; -import dgram from 'dgram'; - -import GrillStatus from '../GrillStatus'; - -const app = express(); -const socket = dgram.createSocket("udp4"); - -const IP = "192.168.86.134"; -const PORT = 8080; - -const commands = Object.freeze({ - powerOn: 'UK001!', - // powerOff: 'UK004!', - getGrillStatus: 'UR001!', - getGrillId: 'UL!', - // setGrillTempF: (temp) => `UT${temp}!`, - setFoodTempF: (temp) => `UF${temp}!` -}); - -// socket.setBroadcast(true); - -socket.on("message", (msg, info) => { - console.log('message', msg, info); - const buff = Buffer.from(msg); - // console.log('message', buff.toString()); - const status = new GrillStatus(buff); - console.log(status);. -}); - -const doSend = (message) => { - const data = Buffer.from(message, 'ascii') - socket.send(data, 0, data.byteLength, PORT, IP, (error) => { - console.error(error); - }); -} - -// doSend(commands.getGrillId); -doSend(commands.getGrillStatus); - -app.get("/", function (req, res) { - res.send("Hello World!"); -}); -app.listen(3000, function () { - console.log("Example app listening on port 3000!"); -}); \ No newline at end of file diff --git a/services/eventEmitter.js b/services/eventEmitter.js new file mode 100644 index 0000000..e7be1cf --- /dev/null +++ b/services/eventEmitter.js @@ -0,0 +1,2 @@ +import EventEmitter from 'events'; +export default new EventEmitter(); diff --git a/services/grillOnError.js b/services/grillOnError.js new file mode 100644 index 0000000..52c363b --- /dev/null +++ b/services/grillOnError.js @@ -0,0 +1,5 @@ +import grillSocket from './grillSocket'; + +grillSocket.on("error", (err) => { + console.error("error", err); +}); \ No newline at end of file diff --git a/services/grillOnMessage.js b/services/grillOnMessage.js new file mode 100644 index 0000000..caf74a9 --- /dev/null +++ b/services/grillOnMessage.js @@ -0,0 +1,31 @@ +import grillSocket from './grillSocket'; + +grillSocket.on("message", async (msg, info) => { + // console.log('message ===='); + // console.log(msg); + // console.log(Buffer.from(msg).toString()); + // console.log(info); + // console.log('end message ===='); + // @todo check for non grill status messages + const grillStatus = new GrillStatus(Buffer.from(msg)); + console.log(grillStatus); + if (grillStatus.state !== status.state) { + await store.addEvent({}); + } + status = { + timestamp: new Date().getTime(), + ...grillStatus, + }; + // console.log('hex ', status.hex); + // console.log('grillOptions', ' ', status.settings); + // console.log(' '); + // console.log('grilOptions decode', status.grillOptions); + console.log("status.value", status.value); + console.log(status.value); + if (status.value.currentGrillTemp !== NaN) { + // assume valid status + await store.add(status); + // console.clear(); + // console.log(dataToStore); + } +}); diff --git a/services/grillPollStatus.js b/services/grillPollStatus.js new file mode 100644 index 0000000..226c813 --- /dev/null +++ b/services/grillPollStatus.js @@ -0,0 +1,11 @@ +import { PORT, IP, INTERVAL } from "../config"; +import { COMMANDS } from "./constants"; +import grillSocket from './grillSocket'; + +export const grillPollStatus = () => { + const data = Buffer.from(COMMANDS.getGrillStatus, "ascii"); + grillSocket.send(data, 0, data.byteLength, PORT, IP, (error) => {}); + setTimeout(() => { + grillPollStatus(); + }, INTERVAL); +}; diff --git a/services/grillSendCommandOnce.js b/services/grillSendCommandOnce.js new file mode 100644 index 0000000..f3b4a9a --- /dev/null +++ b/services/grillSendCommandOnce.js @@ -0,0 +1,8 @@ +import grillSocket from './grillSocket'; +import { PORT, IP } from "../config"; + +export const grillSendCommandOnce = (message, mode = "ascii") => { + const data = Buffer.from(message, mode); + // @todo catch error + grillSocket.send(data, 0, data.byteLength, PORT, IP, () => {}); +}; diff --git a/services/grillSocket.js b/services/grillSocket.js new file mode 100644 index 0000000..5d8e2a7 --- /dev/null +++ b/services/grillSocket.js @@ -0,0 +1,2 @@ +import dgram from "dgram"; +export default dgram.createSocket("udp4"); \ No newline at end of file diff --git a/services/observableGrillStatus.js b/services/observableGrillStatus.js new file mode 100644 index 0000000..e0aec1b --- /dev/null +++ b/services/observableGrillStatus.js @@ -0,0 +1,36 @@ +import { BehaviorSubject } from "rxjs"; +import GrillStatus from "../models/GrillStatus"; +import { filter, distinctUntilChanged } from "rxjs/operators"; +import eventEmitter from "./eventEmitter"; +import { GRILL_EVENTS } from "../constants"; +const status = new BehaviorSubject(new GrillStatus("")); + +status + .pipe( + filter(({ state }) => state === "on"), + distinctUntilChanged() + ) + .subscribe(() => eventEmitter.emit(GRILL_EVENTS.POWER_ON)); + +status + .pipe( + filter(({ state }) => state === "off"), + distinctUntilChanged() + ) + .subscribe(() => eventEmitter.emit(GRILL_EVENTS.POWER_OFF)); + +status + .pipe( + filter(({ state }) => state === "fan mode"), + distinctUntilChanged() + ) + .subscribe(() => eventEmitter.emit(GRILL_EVENTS.FAN_MODE)); + +status + .pipe( + filter(({ state }) => state === "cold smoke mode"), + distinctUntilChanged() + ) + .subscribe(() => eventEmitter.emit(GRILL_EVENTS.POWER_ON_COLDSMOKE)); + +export default status; diff --git a/services/observableGrillStatus.spec.js b/services/observableGrillStatus.spec.js new file mode 100644 index 0000000..667c15a --- /dev/null +++ b/services/observableGrillStatus.spec.js @@ -0,0 +1,27 @@ +import observableGrillStatus from "./observableGrillStatus"; +import eventEmitter from "./eventEmitter"; +import { GRILL_EVENTS } from "../constants"; + +jest.spyOn(eventEmitter, "emit"); + +describe("grill polling", () => { + beforeEach(() => { + eventEmitter.emit.mockClear(); + }); + it("testing rxjs behaviorsubject", async (done) => { + observableGrillStatus.next({ + state: "on", + }); + expect(observableGrillStatus.value.state).toEqual("on"); + expect(eventEmitter.emit).toHaveBeenCalledWith(GRILL_EVENTS.POWER_ON); + done(); + }); + it("testing rxjs behaviorsubject off", async (done) => { + observableGrillStatus.next({ + state: "off", + }); + expect(observableGrillStatus.value.state).toEqual("off"); + expect(eventEmitter.emit).toHaveBeenCalledWith(GRILL_EVENTS.POWER_OFF); + done(); + }); +}); diff --git a/services/renderDebugChart.js b/services/renderDebugChart.js new file mode 100644 index 0000000..2716b0a --- /dev/null +++ b/services/renderDebugChart.js @@ -0,0 +1,9 @@ +import * as store from "./datastore/pouch"; +import asciichart from 'asciichart'; + +export const renderChart = async () => { + const history = await store.all(); + const data = history.map(({ currentProbe1Temp }) => currentProbe1Temp); + console.clear(); + console.log(asciichart.plot(data, { height: 6 })); +}; From b3c020cbab1c82f91c3ad67729c4d75b32bc4338 Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Sun, 19 Apr 2020 21:55:26 -0700 Subject: [PATCH 4/5] Checking in --- README.md | 8 +++++ config/index.js | 2 +- constants/emitter-events.js | 3 +- datastore/pouch.js | 9 ++++-- index.js | 4 ++- models/GrillStatus.js | 2 ++ services/grillOnError.js | 5 ---- services/grillOnMessage.js | 31 ------------------- services/grillPollStatus.js | 42 ++++++++++++++++++++++---- services/grillSendCommandOnce.js | 24 +++++++++++---- services/grillSocket.js | 2 -- services/observableGrillStatus.js | 34 +-------------------- subscribers/grillStatus.js | 50 +++++++++++++++++++++++++++++++ subscribers/index.js | 24 +++++++++++++++ 14 files changed, 153 insertions(+), 87 deletions(-) delete mode 100644 services/grillOnError.js delete mode 100644 services/grillOnMessage.js delete mode 100644 services/grillSocket.js create mode 100644 subscribers/grillStatus.js create mode 100644 subscribers/index.js diff --git a/README.md b/README.md index 126aa60..e163fe0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ ## TODO update with latest +- [x] knowing each socket connection is different we need to refactor that a bit. Create a socket for pinging status, and in sendCommand create a new socket each time and listen for its response. +- [ ] Use sendGrillCOmmandOnce for polling. Make async. Create grill status still in service. + + +- [ ] Capture other grill errors? See https://github.com/Aenima4six2/gmg/commit/614589cf775422e394f4529b4befbe8ac33bbdf0 + +‭ + ## Structure This project is based on structure in this article: https://softwareontheroad.com/ideal-nodejs-project-structure/. A quick read over will help you better navigate this project. diff --git a/config/index.js b/config/index.js index 862b060..0bb99ff 100644 --- a/config/index.js +++ b/config/index.js @@ -1,3 +1,3 @@ -export const IP = "192.168.86.134"; +export const IP = "192.168.86.140"; export const PORT = 8080; export const INTERVAL = 1000; \ No newline at end of file diff --git a/constants/emitter-events.js b/constants/emitter-events.js index 4300d27..5aeb819 100644 --- a/constants/emitter-events.js +++ b/constants/emitter-events.js @@ -1,2 +1,3 @@ // @todo remove, not needed? -export const NEW_GRILL_EVENT = 'NEW_GRILL_EVENT'; \ No newline at end of file +export const NEW_GRILL_EVENT = 'NEW_GRILL_EVENT'; +export const NEW_GRILL_STATUS = 'NEW_GRILL_STATUS'; \ No newline at end of file diff --git a/datastore/pouch.js b/datastore/pouch.js index 2512a71..f7d3551 100644 --- a/datastore/pouch.js +++ b/datastore/pouch.js @@ -1,7 +1,8 @@ var PouchDB = require("pouchdb"); const uuid = require("uuid/v4"); -const db = new PouchDB("http://localhost:5984/api_test_1"); +const db = new PouchDB("http://localhost:5984/api_test_2"); +const eventsDb = new PouchDB("http://localhost:5984/events_test_1"); export async function add(item) { return await db @@ -17,8 +18,10 @@ export async function all() { .then((data) => data.rows.map(({ doc }) => doc).reverse()); } -export async function addEvent() { - return await db.allDocs({ include_docs: true, limit: 1, descending: true }); +export async function addEvent(item) { + return await eventsDb + .put({ _id: `${item.timestamp || new Date().getTime()}`, ...item }) + .then(null, (err) => console.error(err)); } /** diff --git a/index.js b/index.js index fff83e7..208be87 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,10 @@ -// import { pollStatus } from "./grill-polling"; +import './subscribers/grillStatus'; +import { grillPollStatus } from "./services/grillPollStatus"; const app = require("./app.js"); // @TODO start polling here +grillPollStatus(); app.listen(3000, function () { console.log("Example app listening on port 3000!"); diff --git a/models/GrillStatus.js b/models/GrillStatus.js index c3f385a..81716d9 100644 --- a/models/GrillStatus.js +++ b/models/GrillStatus.js @@ -17,6 +17,7 @@ const getGrillState = (hex) => { else if (status === 1) status = "on"; else if (status === 2) status = "fan mode"; else if (status === 3) status = "cold smoke mode"; + // @todo set percent and not temperature when in cold smoke mode else status = "unknown"; return status; }; @@ -71,6 +72,7 @@ const parseHex = (str) => { class GrillStatus { constructor(bytes) { + this.timestamp = new Date().getTime(); const hex = Buffer.from(bytes).toString("hex"); this.state = getGrillState(hex); this.settings = hex.substring(17, 32); diff --git a/services/grillOnError.js b/services/grillOnError.js deleted file mode 100644 index 52c363b..0000000 --- a/services/grillOnError.js +++ /dev/null @@ -1,5 +0,0 @@ -import grillSocket from './grillSocket'; - -grillSocket.on("error", (err) => { - console.error("error", err); -}); \ No newline at end of file diff --git a/services/grillOnMessage.js b/services/grillOnMessage.js deleted file mode 100644 index caf74a9..0000000 --- a/services/grillOnMessage.js +++ /dev/null @@ -1,31 +0,0 @@ -import grillSocket from './grillSocket'; - -grillSocket.on("message", async (msg, info) => { - // console.log('message ===='); - // console.log(msg); - // console.log(Buffer.from(msg).toString()); - // console.log(info); - // console.log('end message ===='); - // @todo check for non grill status messages - const grillStatus = new GrillStatus(Buffer.from(msg)); - console.log(grillStatus); - if (grillStatus.state !== status.state) { - await store.addEvent({}); - } - status = { - timestamp: new Date().getTime(), - ...grillStatus, - }; - // console.log('hex ', status.hex); - // console.log('grillOptions', ' ', status.settings); - // console.log(' '); - // console.log('grilOptions decode', status.grillOptions); - console.log("status.value", status.value); - console.log(status.value); - if (status.value.currentGrillTemp !== NaN) { - // assume valid status - await store.add(status); - // console.clear(); - // console.log(dataToStore); - } -}); diff --git a/services/grillPollStatus.js b/services/grillPollStatus.js index 226c813..ea2186a 100644 --- a/services/grillPollStatus.js +++ b/services/grillPollStatus.js @@ -1,10 +1,42 @@ +import dgram from "dgram"; import { PORT, IP, INTERVAL } from "../config"; -import { COMMANDS } from "./constants"; -import grillSocket from './grillSocket'; +import observableGrillStatus from "../services/observableGrillStatus"; +import GrillStatus from "../models/GrillStatus"; +import { COMMANDS } from "../constants"; +import eventEmitter from "./eventEmitter"; +import { grillSendCommandOnce } from "./grillSendCommandOnce"; + +// export const grillPollStatus = () => { +// const socket = dgram.createSocket("udp4"); +// const data = Buffer.from(COMMANDS.getGrillStatus, "ascii"); + +// socket.on("message", async (msg, info) => { +// console.log(msg.toString()); +// console.log(info); +// observableGrillStatus.next(new GrillStatus(Buffer.from(msg))); +// }); + +// socket.on("error", (msg, info) => { +// console.error("POLL error"); +// }); + +// // @TODO catch error +// const doSendNow = () => { +// socket.send(data, 0, data.byteLength, PORT, IP, (err) => {}); +// }; + +// setInterval(() => { +// doSendNow(); +// }, INTERVAL); + +// doSendNow(); +// }; + +export const grillPollStatus = async () => { + const msg = await grillSendCommandOnce(COMMANDS.getGrillStatus, "ascii"); + observableGrillStatus.next(new GrillStatus(Buffer.from(msg))); + console.log(msg); -export const grillPollStatus = () => { - const data = Buffer.from(COMMANDS.getGrillStatus, "ascii"); - grillSocket.send(data, 0, data.byteLength, PORT, IP, (error) => {}); setTimeout(() => { grillPollStatus(); }, INTERVAL); diff --git a/services/grillSendCommandOnce.js b/services/grillSendCommandOnce.js index f3b4a9a..5b1c85c 100644 --- a/services/grillSendCommandOnce.js +++ b/services/grillSendCommandOnce.js @@ -1,8 +1,22 @@ -import grillSocket from './grillSocket'; +import dgram from "dgram"; import { PORT, IP } from "../config"; -export const grillSendCommandOnce = (message, mode = "ascii") => { - const data = Buffer.from(message, mode); - // @todo catch error - grillSocket.send(data, 0, data.byteLength, PORT, IP, () => {}); +export const grillSendCommandOnce = async (message, mode = "ascii") => { + return new Promise((resolve, reject) => { + const socket = dgram.createSocket("udp4"); + const data = Buffer.from(message, mode); + // @todo catch error + // @todo check info for address / port make sure its not originating from us + // I have not actually seen this happen... + socket.on("message", (msg, info) => { + socket.close(); + resolve(msg); + }); + socket.on("error", (msg, info) => { + console.error(message, msg.toString()); + socket.close(); + reject(msg.toString()); + }); + socket.send(data, 0, data.byteLength, PORT, IP, () => {}); + }); }; diff --git a/services/grillSocket.js b/services/grillSocket.js deleted file mode 100644 index 5d8e2a7..0000000 --- a/services/grillSocket.js +++ /dev/null @@ -1,2 +0,0 @@ -import dgram from "dgram"; -export default dgram.createSocket("udp4"); \ No newline at end of file diff --git a/services/observableGrillStatus.js b/services/observableGrillStatus.js index e0aec1b..f504f71 100644 --- a/services/observableGrillStatus.js +++ b/services/observableGrillStatus.js @@ -1,36 +1,4 @@ import { BehaviorSubject } from "rxjs"; import GrillStatus from "../models/GrillStatus"; -import { filter, distinctUntilChanged } from "rxjs/operators"; -import eventEmitter from "./eventEmitter"; -import { GRILL_EVENTS } from "../constants"; -const status = new BehaviorSubject(new GrillStatus("")); -status - .pipe( - filter(({ state }) => state === "on"), - distinctUntilChanged() - ) - .subscribe(() => eventEmitter.emit(GRILL_EVENTS.POWER_ON)); - -status - .pipe( - filter(({ state }) => state === "off"), - distinctUntilChanged() - ) - .subscribe(() => eventEmitter.emit(GRILL_EVENTS.POWER_OFF)); - -status - .pipe( - filter(({ state }) => state === "fan mode"), - distinctUntilChanged() - ) - .subscribe(() => eventEmitter.emit(GRILL_EVENTS.FAN_MODE)); - -status - .pipe( - filter(({ state }) => state === "cold smoke mode"), - distinctUntilChanged() - ) - .subscribe(() => eventEmitter.emit(GRILL_EVENTS.POWER_ON_COLDSMOKE)); - -export default status; +export default new BehaviorSubject(new GrillStatus("")); diff --git a/subscribers/grillStatus.js b/subscribers/grillStatus.js new file mode 100644 index 0000000..018efcd --- /dev/null +++ b/subscribers/grillStatus.js @@ -0,0 +1,50 @@ +import { isEqual } from "lodash"; +import { + filter, + distinctUntilChanged, + skip, + pluck +} from "rxjs/operators"; +import { GRILL_EVENTS } from "../constants"; +import { addEvent } from "../datastore/pouch"; +import observableGrillStatus from "../services/observableGrillStatus"; + +observableGrillStatus.pipe(skip(1)).subscribe(({ state }) => { + // console.log(" "); + // console.log(" "); + console.log(">>", state); + // console.log(" "); + // console.log(" "); +}); + +const stateChanges = observableGrillStatus.pipe( + pluck("state"), + distinctUntilChanged() +); + +stateChanges.pipe(filter((state) => state === "on")).subscribe(() => { + addEvent({ + type: GRILL_EVENTS.POWER_ON, + }); +}); + +stateChanges.pipe(filter((state) => state === "off")).subscribe(() => { + addEvent({ + type: GRILL_EVENTS.POWER_OFF, + }); +}); + +stateChanges.pipe(filter((state) => state === "fan mode")).subscribe(() => { + addEvent({ + type: GRILL_EVENTS.FAN_MODE, + }); +}); + +stateChanges + .pipe(filter((state) => state === "cold smoke mode")) + .subscribe(() => { + addEvent({ + type: GRILL_EVENTS.POWER_ON_COLDSMOKE, + // @todo add percent? + }); + }); diff --git a/subscribers/index.js b/subscribers/index.js new file mode 100644 index 0000000..8fc1040 --- /dev/null +++ b/subscribers/index.js @@ -0,0 +1,24 @@ +import eventEmitter from "../services/eventEmitter"; +import { addEvent } from "../datastore/pouch"; +import { GRILL_EVENTS, EMITTER_EVENTS } from "../constants"; + +eventEmitter.on(EMITTER_EVENTS.NEW_GRILL_STATUS, (status) => { + console.log(" "); + console.log(" "); + console.log(">>", status); + console.log(" "); + console.log(" "); +}); + +// @todo refactor this to use EMITTER_EVENTS +eventEmitter.on(GRILL_EVENTS.POWER_ON, () => { + addEvent({ + type: GRILL_EVENTS.POWER_ON, + }); +}); + +eventEmitter.on(GRILL_EVENTS.POWER_OFF, () => { + addEvent({ + type: GRILL_EVENTS.POWER_OFF, + }); +}); From b7765eadd7668f7ea8792ac895584441bf6b8ffc Mon Sep 17 00:00:00 2001 From: Matt Miller Date: Sat, 2 May 2020 09:55:13 -0700 Subject: [PATCH 5/5] Checking in --- README.md | 23 ++++ config/index.js | 2 +- constants/grill-events.js | 1 + datastore/pouch.js | 12 ++- index.js | 1 + package-lock.json | 68 +++++++++--- package.json | 1 + services/grillPollStatus.js | 16 ++- services/grillSendCommandOnce.js | 7 +- services/observableGrillStatus.js | 10 +- subscribers/grillStatus.js | 45 ++++++-- subscribers/marbles.spec.js | 168 ++++++++++++++++++++++++++++++ subscribers/process.js | 25 +++++ 13 files changed, 350 insertions(+), 29 deletions(-) create mode 100644 subscribers/marbles.spec.js create mode 100644 subscribers/process.js diff --git a/README.md b/README.md index e163fe0..d1a4336 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,30 @@ ## TODO update with latest + +- [ ] Create clean table structure and files structure + +``` +events +status - optimized what we save every interval? Start with saving it all. +debug - save everything evey tick. Can be blown away at any time. +``` + +- [ ] Make observable grill status a dir, and break out individual observables which can be tested with marbles. +- [ ] Subsribers very leightweight, connect to logging or to datastore events. +- [ ] Add a unit test to demo a hex / message into an expected grill status, so we can make sure we are storing the right data to do this in future. + + +Then + +- [ ] Update chart to visualize events (mark series) and status (line charts) + + +--------- + + - [x] knowing each socket connection is different we need to refactor that a bit. Create a socket for pinging status, and in sendCommand create a new socket each time and listen for its response. - [ ] Use sendGrillCOmmandOnce for polling. Make async. Create grill status still in service. + - Seems to have other errors with this - not sure if its better to open one long polling or to keep opening many shot polls. - [ ] Capture other grill errors? See https://github.com/Aenima4six2/gmg/commit/614589cf775422e394f4529b4befbe8ac33bbdf0 diff --git a/config/index.js b/config/index.js index 0bb99ff..1970424 100644 --- a/config/index.js +++ b/config/index.js @@ -1,3 +1,3 @@ export const IP = "192.168.86.140"; export const PORT = 8080; -export const INTERVAL = 1000; \ No newline at end of file +export const INTERVAL = 2000; \ No newline at end of file diff --git a/constants/grill-events.js b/constants/grill-events.js index 09e68af..e07c36c 100644 --- a/constants/grill-events.js +++ b/constants/grill-events.js @@ -2,3 +2,4 @@ export const POWER_ON = "POWER_ON"; export const POWER_ON_COLDSMOKE = "POWER_ON_COLDSMOKE"; export const FAN_MODE = "FAN_MODE"; export const POWER_OFF = "POWER_OFF"; +export const PROBE_DESIRED_CHANGE = "PROBE_DESIRED_CHANGE"; diff --git a/datastore/pouch.js b/datastore/pouch.js index f7d3551..4fc212b 100644 --- a/datastore/pouch.js +++ b/datastore/pouch.js @@ -1,7 +1,7 @@ var PouchDB = require("pouchdb"); const uuid = require("uuid/v4"); -const db = new PouchDB("http://localhost:5984/api_test_2"); +const db = new PouchDB("http://localhost:5984/api_test_1"); const eventsDb = new PouchDB("http://localhost:5984/events_test_1"); export async function add(item) { @@ -10,7 +10,11 @@ export async function add(item) { .then(null, (err) => console.error(err)); } -export async function last() {} +export async function lastKnownStatus() { + return await db + .allDocs({ include_docs: true, limit: 1, descending: true }) + .then((data) => data.rows.length ? data.rows[0] : null); +} export async function all() { return await db @@ -19,8 +23,10 @@ export async function all() { } export async function addEvent(item) { + console.log('addEvent', item); + const _id = `${item.timestamp || new Date().getTime()}-${item.type}` return await eventsDb - .put({ _id: `${item.timestamp || new Date().getTime()}`, ...item }) + .put({ _id, ...item }) .then(null, (err) => console.error(err)); } diff --git a/index.js b/index.js index 208be87..b9dbd90 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ import './subscribers/grillStatus'; +// import './subscribers/process'; import { grillPollStatus } from "./services/grillPollStatus"; const app = require("./app.js"); diff --git a/package-lock.json b/package-lock.json index 3086d19..f00bcaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2386,6 +2386,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, + "optional": true, "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -2404,6 +2405,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, + "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -2415,6 +2417,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, + "optional": true, "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -2427,6 +2430,7 @@ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, + "optional": true, "requires": { "is-extendable": "^0.1.0" } @@ -2454,7 +2458,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2475,12 +2480,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2495,17 +2502,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2622,7 +2632,8 @@ "inherits": { "version": "2.0.4", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2634,6 +2645,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2648,6 +2660,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2655,12 +2668,14 @@ "minimist": { "version": "1.2.5", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.9.0", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2679,6 +2694,7 @@ "version": "0.5.3", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "^1.2.5" } @@ -2740,7 +2756,8 @@ "npm-normalize-package-bin": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "npm-packlist": { "version": "1.4.8", @@ -2768,7 +2785,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2780,6 +2798,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2857,7 +2876,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2893,6 +2913,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2912,6 +2933,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2955,12 +2977,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -2969,6 +2993,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2" }, @@ -2978,6 +3003,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -3011,6 +3037,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, + "optional": true, "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -3952,6 +3979,12 @@ "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", "dev": true }, + "fast-equals": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-2.0.0.tgz", + "integrity": "sha512-u6RBd8cSiLLxAiC04wVsLV6GBFDOXcTCgWkd3wEoFXgidPSoAJENqC9m7Jb2vewSvjBIfXV6icKeh3GTKfIaXA==", + "dev": true + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -7506,6 +7539,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2" }, @@ -7515,6 +7549,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -7838,6 +7873,15 @@ "tslib": "^1.9.0" } }, + "rxjs-marbles": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rxjs-marbles/-/rxjs-marbles-6.0.0.tgz", + "integrity": "sha512-d9EzhUc0VWeY8OaF/qpZNmunelzE6tml+dOPlyJ7bUWFnmUrQuHAvIOP9C5eJil36dkANNqna1Ev/aPtAd5joA==", + "dev": true, + "requires": { + "fast-equals": "^2.0.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", diff --git a/package.json b/package.json index 53ece28..ea93ddb 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "babel-jest": "^25.3.0", "cross-env": "^7.0.2", "jest": "^25.3.0", + "rxjs-marbles": "^6.0.0", "supertest": "^4.0.2" }, "jest": { diff --git a/services/grillPollStatus.js b/services/grillPollStatus.js index ea2186a..bb3bbb7 100644 --- a/services/grillPollStatus.js +++ b/services/grillPollStatus.js @@ -35,9 +35,23 @@ import { grillSendCommandOnce } from "./grillSendCommandOnce"; export const grillPollStatus = async () => { const msg = await grillSendCommandOnce(COMMANDS.getGrillStatus, "ascii"); observableGrillStatus.next(new GrillStatus(Buffer.from(msg))); - console.log(msg); setTimeout(() => { grillPollStatus(); }, INTERVAL); }; + + +/** + * export const grillPollStatus = () => { + setInterval(() => { + _grillPollStatus(); + }, INTERVAL); +}; + +const _grillPollStatus = async () => { + const msg = await grillSendCommandOnce(COMMANDS.getGrillStatus, "ascii"); + observableGrillStatus.next(new GrillStatus(Buffer.from(msg))); +} + + */ \ No newline at end of file diff --git a/services/grillSendCommandOnce.js b/services/grillSendCommandOnce.js index 5b1c85c..5f7403d 100644 --- a/services/grillSendCommandOnce.js +++ b/services/grillSendCommandOnce.js @@ -9,6 +9,7 @@ export const grillSendCommandOnce = async (message, mode = "ascii") => { // @todo check info for address / port make sure its not originating from us // I have not actually seen this happen... socket.on("message", (msg, info) => { + console.log(msg, info); socket.close(); resolve(msg); }); @@ -17,6 +18,10 @@ export const grillSendCommandOnce = async (message, mode = "ascii") => { socket.close(); reject(msg.toString()); }); - socket.send(data, 0, data.byteLength, PORT, IP, () => {}); + socket.send(data, 0, data.byteLength, PORT, IP, (err) => { + if (err) { + console.error(err); + } + }); }); }; diff --git a/services/observableGrillStatus.js b/services/observableGrillStatus.js index f504f71..d383419 100644 --- a/services/observableGrillStatus.js +++ b/services/observableGrillStatus.js @@ -1,4 +1,12 @@ import { BehaviorSubject } from "rxjs"; import GrillStatus from "../models/GrillStatus"; +import { lastKnownStatus } from "../datastore/pouch"; -export default new BehaviorSubject(new GrillStatus("")); +// (async () => { +// return await lastKnownStatus(); +// })(); + +// console.warn(status); + +// @todo model should have a `fromJson` method? +export default new BehaviorSubject(""); diff --git a/subscribers/grillStatus.js b/subscribers/grillStatus.js index 018efcd..29cc6e2 100644 --- a/subscribers/grillStatus.js +++ b/subscribers/grillStatus.js @@ -1,27 +1,52 @@ -import { isEqual } from "lodash"; -import { - filter, - distinctUntilChanged, - skip, - pluck -} from "rxjs/operators"; +import { filter, distinctUntilChanged, skip, pluck, tap } from "rxjs/operators"; import { GRILL_EVENTS } from "../constants"; import { addEvent } from "../datastore/pouch"; import observableGrillStatus from "../services/observableGrillStatus"; -observableGrillStatus.pipe(skip(1)).subscribe(({ state }) => { +observableGrillStatus.pipe(skip(1)).subscribe((status) => { // console.log(" "); // console.log(" "); - console.log(">>", state); + console.clear(); + console.log(">>", status); // console.log(" "); // console.log(" "); }); const stateChanges = observableGrillStatus.pipe( pluck("state"), - distinctUntilChanged() + distinctUntilChanged(), + skip(2) ); +export const probe1Changes = (subject) => + subject.pipe( + pluck("desiredProbe1Temp"), + distinctUntilChanged(), + skip(2) + ); + +const probe2Changes = observableGrillStatus.pipe( + pluck("desiredProbe2Temp"), + distinctUntilChanged(), + skip(2) +); + +probe1Changes(observableGrillStatus).subscribe((temp) => { + addEvent({ + type: GRILL_EVENTS.PROBE_DESIRED_CHANGE, + probe: 1, + temp, + }); +}); + +probe2Changes.subscribe((temp) => { + addEvent({ + type: GRILL_EVENTS.PROBE_DESIRED_CHANGE, + probe: 2, + temp, + }); +}); + stateChanges.pipe(filter((state) => state === "on")).subscribe(() => { addEvent({ type: GRILL_EVENTS.POWER_ON, diff --git a/subscribers/marbles.spec.js b/subscribers/marbles.spec.js new file mode 100644 index 0000000..16b9048 --- /dev/null +++ b/subscribers/marbles.spec.js @@ -0,0 +1,168 @@ +import { marbles } from "rxjs-marbles/jest"; +import { + pluck, + map, + concat, + distinctUntilChanged, + skip, + tap, +} from "rxjs/operators"; +import { probe1Changes } from "./grillStatus"; +import { BehaviorSubject } from "rxjs"; +// import observableGrillStatus from "../services/observableGrillStatus"; + +// jest.mock("../services/observableGrillStatus", () => jest.fn()); + +describe("learn", () => { + it( + "probe1Changes", + marbles((m) => { + const sValues = { + a: { probe1Changes: undefined }, + b: { probe1Changes: 100 }, + c: { probe1Changes: 100 }, + d: { probe1Changes: 150 }, + }; + const eValues = { d: 150 }; + const source = m.hot(" ^-a-b-c-d-d-|", sValues); + const expected = m.cold(" --------d---|", eValues); + const dest = source.pipe( + pluck("probe1Changes"), + distinctUntilChanged(), + skip(2), + tap((v) => console.log(v)) + ); + m.expect(dest).toBeObservable(expected); + }) + ); + it( + "probe1Changes with sharedGrillState", + marbles((m) => { + const sValues = { + a: { desiredProbe1Temp: "1" }, + b: { desiredProbe1Temp: 100 }, + c: { desiredProbe1Temp: 100 }, + d: { desiredProbe1Temp: 150 }, + }; + const eValues = { d: 150 }; + const source = m.hot(" ^-a-b-c-d-d-|", sValues); + const expected = m.cold(" --------d---|", eValues); + const dest = probe1Changes(source); + m.expect(dest).toBeObservable(expected); + }) + ); + it( + "parses", + marbles((m) => { + const source = m.cold("---a---b---|"); + const expected = " ---a---b---|"; + m.expect(source).toBeObservable(expected); + }) + ); + it( + "distinctUntilChanged", + marbles((m) => { + const source = m.hot("--a---a-^-a---a---|"); + const expected = " --a-------|"; + const dest = source.pipe(distinctUntilChanged()); + m.expect(dest).toBeObservable(expected); + }) + ); + it( + "distinctUntilChanged but skips them all", + marbles((m) => { + const source = m.hot("-^-a---b---a---a---|"); + const expected = " ------b---a-------|"; + const dest = source.pipe(distinctUntilChanged(), skip(1)); + m.expect(dest).toBeObservable(expected); + }) + ); + it( + "hot example", + marbles((m) => { + const obs1 = m.hot("---a-^-b---|"); + const obs2 = m.hot("----c^------d--|"); + const expected = " --b----d--|"; + const dest = obs1.pipe(concat(obs2)); + m.expect(dest).toBeObservable(expected); + }) + ); +}); + +describe("basic", () => { + it( + "should support marble tests without values", + marbles((m) => { + const source = m.hot(" --^-a-b-c-|"); + const subs = " ^-------!"; + const expected = m.cold(" --b-c-d-|"); + + const destination = source.pipe( + map((value) => String.fromCharCode(value.charCodeAt(0) + 1)) + ); + m.expect(destination).toBeObservable(expected); + m.expect(source).toHaveSubscriptions(subs); + }) + ); + + it( + "should support marble tests with values", + marbles((m) => { + const inputs = { + a: 1, + b: 2, + c: 3, + }; + const outputs = { + x: 2, + y: 3, + z: 4, + }; + + const source = m.hot(" --^-a-b-c-|", inputs); + const subs = " ^-------!"; + const expected = m.cold(" --x-y-z-|", outputs); + + const destination = source.pipe(map((value) => value + 1)); + m.expect(destination).toBeObservable(expected); + m.expect(source).toHaveSubscriptions(subs); + }) + ); + + it( + "should support marble tests with errors", + marbles((m) => { + const source = m.hot(" --^-a-b-c-#"); + const subs = " ^-------!"; + const expected = m.cold(" --a-b-c-#"); + + const destination = source; + m.expect(destination).toBeObservable(expected); + m.expect(source).toHaveSubscriptions(subs); + }) + ); + + it( + "should support marble tests with explicit errors", + marbles((m) => { + const inputs = { + a: 1, + b: 2, + c: 3, + }; + const outputs = { + x: 2, + y: 3, + z: 4, + }; + + const source = m.hot(" --^-a-b-c-#", inputs, new Error("Boom!")); + const subs = " ^-------!"; + const expected = m.cold(" --x-y-z-#", outputs, new Error("Boom!")); + + const destination = source.pipe(map((value) => value + 1)); + m.expect(destination).toBeObservable(expected); + m.expect(source).toHaveSubscriptions(subs); + }) + ); +}); diff --git a/subscribers/process.js b/subscribers/process.js new file mode 100644 index 0000000..a9160c3 --- /dev/null +++ b/subscribers/process.js @@ -0,0 +1,25 @@ +import process from "process"; +import { lastKnownStatus } from "../datastore/pouch"; + +process.stdin.resume(); //so the program will not close instantly + +async function exitHandler(options, exitCode) { + const status = await lastKnownStatus(); + console.log('status', status); + if (options.cleanup) console.log("clean"); + if (exitCode || exitCode === 0) console.log(exitCode); + if (options.exit) process.exit(); +} + +//do something when app is closing +process.on("exit", exitHandler.bind(null, { cleanup: true })); + +//catches ctrl+c event +process.on("SIGINT", exitHandler.bind(null, { exit: true })); + +// catches "kill pid" (for example: nodemon restart) +process.on("SIGUSR1", exitHandler.bind(null, { exit: true })); +process.on("SIGUSR2", exitHandler.bind(null, { exit: true })); + +//catches uncaught exceptions +process.on("uncaughtException", exitHandler.bind(null, { exit: true }));