diff --git a/src/back/config.ts b/src/back/config.ts index 38515c1..05cec5b 100644 --- a/src/back/config.ts +++ b/src/back/config.ts @@ -15,3 +15,15 @@ export const MAILCATCHER_API_PORT = parseInt( process.env.SEALIOUS_MAILCATCHER_API_PORT || "1082" ); export const MAILER = process.env.SEALIOUS_MAILER; +export const HA_TOKEN = process.env.HA_TOKEN; +export const HA_URL = process.env.HA_URL; + +if (!HA_TOKEN) { + console.error("Please set HA_TOKEN env variable to communicate with Home Assistant"); + process.exit(1); +} + +if (!HA_URL) { + console.error("Please set HA_URL env variable to communicate with Home Assistant"); + process.exit(1); +} diff --git a/src/back/ha_api.ts b/src/back/ha_api.ts new file mode 100644 index 0000000..e6fcd08 --- /dev/null +++ b/src/back/ha_api.ts @@ -0,0 +1,14 @@ +import { HA_TOKEN, HA_URL } from "./config.js"; + +export type HA_RESPONSE = { state: string; attributes: Record }; + +export async function get_state(entity: string): Promise { + const response = (await ( + await fetch(`${HA_URL}/api/states/${entity}`, { + headers: { + Authorization: `Bearer ${HA_TOKEN}`, + }, + }) + ).json()) as HA_RESPONSE; + return response; +} diff --git a/src/back/routes/dashboard.sreact.tsx b/src/back/routes/dashboard.sreact.tsx new file mode 100644 index 0000000..c0d8a86 --- /dev/null +++ b/src/back/routes/dashboard.sreact.tsx @@ -0,0 +1,87 @@ +import { TempstreamJSX, Templatable } from "tempstream"; +import { BaseContext } from "koa"; +import { StatefulPage } from "@sealcode/sealgen"; +import html from "../html.js"; +import { get_state, HA_RESPONSE } from "../ha_api.js"; +import { sleep } from "../util.js"; + +export const actionName = "Dashboard"; + +const actions = { + add: (state: State, inputs: Record) => { + console.log({ inputs }); + return { + ...state, + elements: [...state.elements, inputs.element_to_add || "new element"], + }; + }, + remove: (state: State, _: unknown, index_to_remove: number) => { + return { + ...state, + elements: state.elements.filter((_, index) => index != index_to_remove), + }; + }, +} as const; + +type State = { + elements: string[]; +}; + +async function with_unit(s: HA_RESPONSE | Promise) { + s = await s; + return `${s.state}${s.attributes.unit_of_measurement as string}`; +} + +export default new (class DashboardPage extends StatefulPage { + actions = actions; + + getInitialState() { + return { elements: ["one", "two", "three"] }; + } + + wrapInLayout(ctx: BaseContext, content: Templatable): Templatable { + return html(ctx, "Dashboard", content, { navbar: () => "" }); + } + + render(ctx: BaseContext, state: State, inputs: Record) { + const date = new Date(); + const stuff = [ + { label: "Temperatura na balkonie", entity: "input_number.balkon_average" }, + { label: "Temperatura w salonie", entity: "sensor.ewelink_th01_temperature" }, + { label: "Wilgotność w salonie", entity: "sensor.ewelink_th01_humidity" }, + { label: "Temperatura na strychu", entity: "sensor.strych_temperature_3" }, + { + label: "Docelowa temp. na strychu", + entity: "input_number.strych_target_temperature", + }, + ]; + + return ( +
+
+ {date.getHours()}:{date.getMinutes().toString().padStart(2, "0")} +
+ + + {stuff.map(({ label, entity }) => ( + + + + + ))} + +
{label}: + {with_unit(get_state(entity))} +
+ + { + /* HTML */ `` + } +
+ ); + } +})(); diff --git a/src/back/routes/dashboard.test.ts b/src/back/routes/dashboard.test.ts new file mode 100644 index 0000000..704c680 --- /dev/null +++ b/src/back/routes/dashboard.test.ts @@ -0,0 +1,48 @@ +import { withProdApp } from "../test_utils/with-prod-app.js"; +import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js"; +import { DashboardURL } from "./urls.js"; +import { getBrowser } from "../test_utils/browser-creator.js"; +import { Browser, BrowserContext, Page } from "@playwright/test"; + +describe("Dashboard webhint", () => { + it( + "doesn't crash", + async function () { + return withProdApp(async ({ base_url, rest_api }) => { + await rest_api.get(DashboardURL); + await webhintURL(base_url + DashboardURL); + // alternatively you can use webhintHTML for faster but less precise scans + // or for scanning responses of requests that use some form of authorization: + // const response = await rest_api.get(DashboardURL); + // await webhintHTML(response); + }); + }, + VERY_LONG_TEST_TIMEOUT + ); +}); + +describe("Dashboard", () => { + let page: Page; + let browser: Browser; + let context: BrowserContext; + + beforeEach(async () => { + browser = await getBrowser(); + context = await browser.newContext(); + page = await context.newPage(); + }); + + afterEach(async () => { + await context.close(); + }); + + it( + "works as expected", + async function () { + return withProdApp(async ({ base_url }) => { + await page.goto(base_url + DashboardURL); + }); + }, + VERY_LONG_TEST_TIMEOUT + ); +}); diff --git a/src/back/routes/routes.ts b/src/back/routes/routes.ts index 3da0ddf..bb9f426 100644 --- a/src/back/routes/routes.ts +++ b/src/back/routes/routes.ts @@ -5,6 +5,7 @@ import { mount } from "@sealcode/sealgen"; import * as URLs from "./urls.js"; import { default as Components } from "./components.sreact.js"; +import { default as Dashboard } from "./dashboard.sreact.js"; import { default as Hello } from "./hello.page.js"; import { default as Logout } from "./logout.redirect.js"; import { default as SignIn } from "./signIn.form.js"; @@ -13,6 +14,7 @@ import { default as Todo } from "./todo.form.js"; export default function mountAutoRoutes(router: Router) { mount(router, URLs.ComponentsURL, Components); + mount(router, URLs.DashboardURL, Dashboard); mount(router, URLs.HelloURL, Hello); mount(router, URLs.LogoutURL, Logout); mount(router, URLs.SignInURL, SignIn); diff --git a/src/back/routes/urls.ts b/src/back/routes/urls.ts index 72d8c70..2bf3e62 100644 --- a/src/back/routes/urls.ts +++ b/src/back/routes/urls.ts @@ -1,4 +1,5 @@ export const ComponentsURL = "/components/"; +export const DashboardURL = "/dashboard/"; export const HelloURL = "/hello/"; export const LogoutURL = "/logout/"; export const SignInURL = "/signIn/"; diff --git a/src/main.css b/src/main.css index 03b7da2..97b7a51 100644 --- a/src/main.css +++ b/src/main.css @@ -1,7 +1,6 @@ @import "includes.css"; html { - background: #eee; font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Segoe UI Emoji", "Segoe UI Symbol", "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; @@ -12,19 +11,6 @@ body { max-width: 1024px; margin: 1rem auto; background: white; + color: black; padding: 1rem; } - -.delete-button { - height: 1rem; - padding: 0; - line-height: 0; - padding: 0.5rem; -} - -.nav-logo { - display: flex; - align-items: center; -} - -