Adds application
This commit is contained in:
commit
b2dc69142a
12 changed files with 1683 additions and 0 deletions
5
.env.dist
Normal file
5
.env.dist
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
DOMAIN="https://my.home.assistant.tld"
|
||||
TOKEN="TOKEN"
|
||||
|
||||
TEMPERATURE_SENSOR=sensor.temperature_sensor
|
||||
MOISTURE_SENSOR=sensor.moisture_sensor
|
||||
39
.forgejo/workflows/build-release-container.yaml
Normal file
39
.forgejo/workflows/build-release-container.yaml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
name: build-container
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- release-*
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
BUILD_TARGET: DOCKER
|
||||
BUILD_LABEL: release
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: node-20
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
|
||||
- name: Install Docker
|
||||
run: curl https://get.docker.com | sh
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and Push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: neintonine/watchmemelt:${{ env.BUILD_LABEL }}
|
||||
context: .
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.idea/
|
||||
node_modules/
|
||||
dist/
|
||||
17
build/build-cli.mjs
Normal file
17
build/build-cli.mjs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import * as fs from "node:fs";
|
||||
|
||||
if (!process.env.BUILD_TARGET) {
|
||||
process.env.BUILD_TARGET = "LOCAL";
|
||||
}
|
||||
|
||||
if (!process.env.BUILD_LABEL) {
|
||||
process.env.BUILD_LABEL = "development";
|
||||
}
|
||||
|
||||
import context from './context.mjs';
|
||||
|
||||
(async () => {
|
||||
await context.rebuild();
|
||||
await context.dispose();
|
||||
})()
|
||||
|
||||
20
build/context.mjs
Normal file
20
build/context.mjs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import * as esbuild from "esbuild";
|
||||
import path from "path";
|
||||
|
||||
const isDev = process.env.BUILD_TARGET !== 'DOCKER';
|
||||
|
||||
const context = await esbuild.context({
|
||||
entryPoints: [
|
||||
path.join('source', 'main.ts')
|
||||
],
|
||||
bundle: true,
|
||||
outdir: './dist/',
|
||||
platform: 'node',
|
||||
target: 'node10.4',
|
||||
sourcemap: isDev ? 'external' : false,
|
||||
loader: {
|
||||
'.node': 'copy',
|
||||
}
|
||||
})
|
||||
|
||||
export default context
|
||||
3
build/watch.mjs
Normal file
3
build/watch.mjs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import context from './context.mjs'
|
||||
|
||||
await context.watch();
|
||||
1418
package-lock.json
generated
Normal file
1418
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
17
package.json
Normal file
17
package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"scripts": {
|
||||
"build": "node ./build/build-cli.mjs",
|
||||
"watch": "node ./build/watch.mjs",
|
||||
"start": "node ./dist/main.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^24.2.1",
|
||||
"dotenv": "^17.2.1",
|
||||
"esbuild": "^0.25.9",
|
||||
"express": "^5.1.0",
|
||||
"undici": "^7.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.3"
|
||||
}
|
||||
}
|
||||
BIN
public/JetBrainsMono-Medium.woff2
Normal file
BIN
public/JetBrainsMono-Medium.woff2
Normal file
Binary file not shown.
102
public/index.html
Normal file
102
public/index.html
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Watch me melt...</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
src: local("JetBrainsMono"), url("JetBrainsMono-Medium.woff2");
|
||||
}
|
||||
|
||||
:root {
|
||||
--background-color: #f1e1d1;
|
||||
--text-color: black;
|
||||
--muted-color: #777777;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background-color: #282c34;
|
||||
--text-color: #f1e1d1;
|
||||
--muted-color: #bca6a2;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 6rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 3rem;
|
||||
}
|
||||
h3 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
#temp_display :after {
|
||||
content: "C";
|
||||
}
|
||||
|
||||
#moisture :after {
|
||||
content: "%";
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="centered">
|
||||
<h2 style="margin-bottom: 0.5rem">
|
||||
Watch me melt!
|
||||
</h2>
|
||||
|
||||
<span class="muted">in my office...</span>
|
||||
<h3 id="timestamp"></h3>
|
||||
<h1 id="temp_display">0</h1>
|
||||
<h2 id="moisture">0</h2>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const TEMP_DISPLAY = document.querySelector("#temp_display");
|
||||
const MOISTURE = document.querySelector("#moisture");
|
||||
const TIMESTAMP_DISPLAY = document.querySelector("#timestamp");
|
||||
|
||||
async function requestValues() {
|
||||
const result = await fetch('/get');
|
||||
const data = await result.json();
|
||||
|
||||
TIMESTAMP_DISPLAY.textContent = new Date().toLocaleString();
|
||||
TEMP_DISPLAY.textContent = `${data.temperature.toLocaleString(undefined, { minimumFractionDigits: 2 })}°C`;
|
||||
MOISTURE.textContent = `${data.moisture}%`;
|
||||
}
|
||||
|
||||
setInterval(requestValues, 1000);
|
||||
requestValues();
|
||||
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
44
source/main.ts
Normal file
44
source/main.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import express from "express";
|
||||
import dotenv from 'dotenv';
|
||||
import * as fs from "node:fs";
|
||||
import {Agent} from "undici";
|
||||
|
||||
if (fs.existsSync(".env.dist")) {
|
||||
dotenv.config({ path: '.env.dist'});
|
||||
}
|
||||
|
||||
const URI = `${process.env.DOMAIN}/api/states`;
|
||||
|
||||
async function getSensorValue(sensor: string): Promise<number> {
|
||||
const result = await fetch(`${URI}/${sensor}`, {
|
||||
dispatcher: new Agent({
|
||||
connect: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
}),
|
||||
headers: [
|
||||
['Authorization', `Bearer ${process.env.TOKEN}`]
|
||||
]
|
||||
});
|
||||
if (!result.ok) {
|
||||
console.error(result)
|
||||
return -1;
|
||||
}
|
||||
const data: unknown = await result.json();
|
||||
return parseInt(data.state);
|
||||
}
|
||||
|
||||
const server = express()
|
||||
|
||||
server.use("/", express.static("public"));
|
||||
|
||||
server.get('/get', async (req, res) => {
|
||||
const result = {
|
||||
temperature: await getSensorValue(process.env.TEMPERATURE_SENSOR ?? ''),
|
||||
moisture: await getSensorValue(process.env.MOISTURE_SENSOR ?? '')
|
||||
}
|
||||
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
server.listen(8080);
|
||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"_version": "20.1.0",
|
||||
|
||||
"compilerOptions": {
|
||||
"lib": ["es2023"],
|
||||
"module": "node16",
|
||||
"target": "es2022",
|
||||
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "node16"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue