Add dashboard

master
Kuba Orlik 9 months ago
parent ae458c8046
commit 4613025579

@ -15,3 +15,15 @@ export const MAILCATCHER_API_PORT = parseInt(
process.env.SEALIOUS_MAILCATCHER_API_PORT || "1082" process.env.SEALIOUS_MAILCATCHER_API_PORT || "1082"
); );
export const MAILER = process.env.SEALIOUS_MAILER; 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);
}

@ -0,0 +1,14 @@
import { HA_TOKEN, HA_URL } from "./config.js";
export type HA_RESPONSE = { state: string; attributes: Record<string, unknown> };
export async function get_state(entity: string): Promise<HA_RESPONSE> {
const response = (await (
await fetch(`${HA_URL}/api/states/${entity}`, {
headers: {
Authorization: `Bearer ${HA_TOKEN}`,
},
})
).json()) as HA_RESPONSE;
return response;
}

@ -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<string, string>) => {
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<HA_RESPONSE>) {
s = await s;
return `${s.state}${s.attributes.unit_of_measurement as string}`;
}
export default new (class DashboardPage extends StatefulPage<State, typeof actions> {
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<string, string>) {
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 (
<div style="font-size: 2rem">
<div style="font-size: 10rem; text-align: center;">
{date.getHours()}:{date.getMinutes().toString().padStart(2, "0")}
</div>
<table style="margin: 0 auto">
<tbody>
{stuff.map(({ label, entity }) => (
<tr>
<td style="text-align: right">{label}:</td>
<td>
<strong>{with_unit(get_state(entity))}</strong>
</td>
</tr>
))}
</tbody>
</table>
{
/* HTML */ `<script>
setInterval(function () {
document.location = document.location;
}, 15 * 1000);
</script>`
}
</div>
);
}
})();

@ -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
);
});

@ -5,6 +5,7 @@ import { mount } from "@sealcode/sealgen";
import * as URLs from "./urls.js"; import * as URLs from "./urls.js";
import { default as Components } from "./components.sreact.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 Hello } from "./hello.page.js";
import { default as Logout } from "./logout.redirect.js"; import { default as Logout } from "./logout.redirect.js";
import { default as SignIn } from "./signIn.form.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) { export default function mountAutoRoutes(router: Router) {
mount(router, URLs.ComponentsURL, Components); mount(router, URLs.ComponentsURL, Components);
mount(router, URLs.DashboardURL, Dashboard);
mount(router, URLs.HelloURL, Hello); mount(router, URLs.HelloURL, Hello);
mount(router, URLs.LogoutURL, Logout); mount(router, URLs.LogoutURL, Logout);
mount(router, URLs.SignInURL, SignIn); mount(router, URLs.SignInURL, SignIn);

@ -1,4 +1,5 @@
export const ComponentsURL = "/components/"; export const ComponentsURL = "/components/";
export const DashboardURL = "/dashboard/";
export const HelloURL = "/hello/"; export const HelloURL = "/hello/";
export const LogoutURL = "/logout/"; export const LogoutURL = "/logout/";
export const SignInURL = "/signIn/"; export const SignInURL = "/signIn/";

@ -1,7 +1,6 @@
@import "includes.css"; @import "includes.css";
html { html {
background: #eee;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI",
"Segoe UI Emoji", "Segoe UI Symbol", "Lato", "Helvetica Neue", Helvetica, Arial, "Segoe UI Emoji", "Segoe UI Symbol", "Lato", "Helvetica Neue", Helvetica, Arial,
sans-serif; sans-serif;
@ -12,19 +11,6 @@ body {
max-width: 1024px; max-width: 1024px;
margin: 1rem auto; margin: 1rem auto;
background: white; background: white;
color: black;
padding: 1rem; padding: 1rem;
} }
.delete-button {
height: 1rem;
padding: 0;
line-height: 0;
padding: 0.5rem;
}
.nav-logo {
display: flex;
align-items: center;
}

Loading…
Cancel
Save