Skip to content

Commit

Permalink
fix: add connection check (#138)
Browse files Browse the repository at this point in the history
  • Loading branch information
philipparndt authored Jun 11, 2023
1 parent 1eade52 commit 935d0d9
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 10 deletions.
15 changes: 12 additions & 3 deletions app/lib/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import EventSource from "eventsource"
import cron from "node-cron"
import { registerConnectionCheck, unregisterConnectionCheck } from "./connection"
import { log } from "./logger"
import { login, needsRefresh } from "./miele/login/login"
import { smallMessage } from "./miele/miele"
Expand All @@ -9,17 +10,22 @@ import { connectMqtt, publish } from "./mqtt/mqtt-client"
export const triggerFullUpdate = async () => {
if (needsRefresh()) {
log.info("Token refresh required. Reconnecting now.")
eventSource?.close()
await start()
await restart()
}
}

const restart = async () => {
eventSource?.close()
unregisterConnectionCheck()
await start()
}

let eventSource: EventSource

const start = async () => {
const token = await (login())

const { sse, registerDevicesListener } = startSSE(token.access_token)
const { sse, registerDevicesListener } = startSSE(token.access_token, restart)

registerDevicesListener((devices) => {
for (const device of devices) {
Expand All @@ -28,6 +34,8 @@ const start = async () => {
}
})

registerConnectionCheck(restart)

eventSource = sse
}

Expand All @@ -45,6 +53,7 @@ export const startApp = async () => {
return () => {
mqttCleanUp()
eventSource?.close()
unregisterConnectionCheck()
task.stop()
}
}
Expand Down
120 changes: 117 additions & 3 deletions app/lib/config/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
import Duration from "@icholy/duration"
import * as fs from "fs"
import * as os from "os"
import path from "path"
import { applyDefaults, getAppConfig, loadConfig } from "./config"
import { log } from "../logger"
import { add } from "../miele/duration"
// eslint-disable-next-line camelcase
import { __TEST_getToken, setToken } from "../miele/login/login"
import { applyConfig, applyDefaults, getAppConfig, loadConfig, persistToken, recoverToken } from "./config"

describe("Config", () => {
test("default values", async () => {
const config = {
mqtt: {
url: "tcp://192.168.1.1:1883",
topic: "hue"
topic: "miele"
}
}

expect(applyDefaults(config)).toStrictEqual({
loglevel: "info",
miele: {
"connection-check-interval": 10000,
"country-code": "de-DE",
mode: "sse"
},
mqtt: {
"bridge-info": true,
qos: 1,
retain: true,
topic: "hue",
topic: "miele",
url: "tcp://192.168.1.1:1883"
},
"send-full-update": true
Expand Down Expand Up @@ -49,4 +57,110 @@ describe("Config", () => {
loadConfig(path.join(__dirname, "../../../production/config/config-example.json"))
expect(getAppConfig().miele.mode).toBe("sse")
})

describe("Token recover", () => {
beforeEach(() => {
log.off()
setToken(undefined)
})

test("recover token", () => {
applyConfig({
miele: {
token: {
access: "access_token",
refresh: "refresh_token",
validUntil: add(new Date(), Duration.days(7)).toISOString()
}
}
})
recoverToken()
expect(__TEST_getToken()!.access_token).toBe("access_token")
})

test("cannot recover token", () => {
applyConfig({
})
recoverToken()
expect(__TEST_getToken()).toBeFalsy()
})
})

describe("Persist token", () => {
beforeEach(() => {
log.off()
setToken(undefined)
})

const token = {
access: "access-token",
refresh: "refresh-token",
validUntil: "valid-until"
}

test("Persist token", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "miele-mqtt-test"))
const config = path.join(tmp, "config.json")
fs.writeFileSync(config, JSON.stringify({
mqtt: {
url: "tcp://192.168.1.1:1883",
topic: "miele"
},
miele: {

}
}))

loadConfig(config)

persistToken(token)

const data = fs.readFileSync(config)
const configuration = JSON.parse(data.toString("utf-8"))
expect(configuration).toStrictEqual(
{
mqtt: { url: "tcp://192.168.1.1:1883", topic: "miele" },
miele: {
token
}
}
)
})

test("No token change", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "miele-mqtt-test"))
const config = path.join(tmp, "config.json")
fs.writeFileSync(config, JSON.stringify({
mqtt: {
url: "tcp://192.168.1.1:1883",
topic: "miele"
},
miele: {
token
}
}))

loadConfig(config)
fs.writeFileSync(config, JSON.stringify({
miele: {
token
}
}))
persistToken({
access: "access-token",
refresh: "refresh-token",
validUntil: "valid-until"
})

const data = fs.readFileSync(config)
const configuration = JSON.parse(data.toString("utf-8"))
expect(configuration).toStrictEqual(
{
miele: {
token
}
}
)
})
})
})
5 changes: 4 additions & 1 deletion app/lib/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export type ConfigMiele = {
"polling-interval"?: number

token?: ConfigToken

"connection-check-interval": number
}

export type Config = {
Expand All @@ -52,7 +54,8 @@ const mqttDefaults = {

const mieleDefaults = {
mode: "sse",
"country-code": "de-DE"
"country-code": "de-DE",
"connection-check-interval": 10_000
}

const configDefaults = {
Expand Down
39 changes: 39 additions & 0 deletions app/lib/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import dns from "dns"
import { getAppConfig } from "./config/config"
import { log } from "./logger"

let checkConnection: ReturnType<typeof setTimeout>
let connectionLost = false

export const unregisterConnectionCheck = () => {
checkConnection?.unref()
}

export const registerConnectionCheck = (restartHook: () => Promise<void>, config = getAppConfig().miele) => {
const interval = config["connection-check-interval"]
if (interval === 0) {
log.debug("Internet connection check disabled")
return
}
log.info("Internet connection will be checked every", { ms: interval })
connectionLost = false
checkConnection = setInterval(() => {
log.debug("Checking connection")
dns.resolve("api.mcs3.miele.com", (err) => {
if (err) {
log.debug("Connection check failed", err)
if (!connectionLost) {
connectionLost = true
log.error("Connection lost. Waiting for connection to come back.", err)
}
}
else if (connectionLost) {
log.debug("Connection check success after connection was lost")
restartHook().then()
}
else {
log.debug("Connection check success")
}
})
}, interval)
}
5 changes: 4 additions & 1 deletion app/lib/miele/login/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { fetchToken, Token, TokenResult } from "./token"

let token: Token | undefined

export const setToken = (newToken: Token) => {
// eslint-disable-next-line camelcase
export const __TEST_getToken = () => token

export const setToken = (newToken: Token | undefined) => {
token = newToken
}

Expand Down
3 changes: 3 additions & 0 deletions app/lib/miele/miele.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { JEST_INTEGRATION_TIMEOUT } from "../../test/test-utils"
import { applyConfig } from "../config/config"
import { log } from "../logger"
import { getToken } from "./login/login"
import { fetchDevices, smallMessage } from "./miele"
import { testConfig } from "./miele-testutils"

jest.setTimeout(JEST_INTEGRATION_TIMEOUT)

describe("miele", () => {
beforeAll(() => {
applyConfig(testConfig())
Expand Down
2 changes: 1 addition & 1 deletion app/lib/miele/sse-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("sse-client", () => {

test("integration", async () => {
const token = await getToken()
const { sse, registerDevicesListener } = startSSE(token.access_token)
const { sse, registerDevicesListener } = startSSE(token.access_token, () => Promise.resolve())

const devices = await new Promise<MieleDevice[]>((resolve) => {
registerDevicesListener(devices => {
Expand Down
8 changes: 7 additions & 1 deletion app/lib/miele/sse-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { MieleDevice } from "./miele-types"

type DevicesListener = (devices: MieleDevice[]) => void

export const startSSE = (token: string) => {
export const startSSE = (token: string, restart: () => Promise<void>) => {
log.info("Starting Server-Sent events")

const eventSourceInitDict = {
Expand All @@ -17,11 +17,17 @@ export const startSSE = (token: string) => {
}

const sse = new EventSource("https://api.mcs3.miele.com/v1/devices/all/events", eventSourceInitDict)
// setInterval(() => log.debug("SSE", sse), 1000)
sse.onerror = (err: any) => {
if (err) {
log.error("SSE error", err)
log.info("Restarting SSE")
restart().then()
}
}
sse.addEventListener("open", () => log.info("SSE connection opened"))
sse.addEventListener("error", (err) => log.error("SSE error", err))
sse.addEventListener("close", () => log.info("SSE connection closed"))

const registerDevicesListener = (listener: DevicesListener) => {
sse.addEventListener("devices", (event) => listener(convertDevices(JSON.parse(event.data))))
Expand Down

0 comments on commit 935d0d9

Please sign in to comment.