Skip to content

Commit 19e921b

Browse files
author
3aa49ec6bfc910647fa1c5a013e48eef
committed
Added custom LockChime support
1 parent 027e476 commit 19e921b

File tree

11 files changed

+1318
-4
lines changed

11 files changed

+1318
-4
lines changed

src/website/package-lock.json

Lines changed: 1058 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/website/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,26 @@
1616
"@sveltejs/adapter-node": "^2.0.1",
1717
"@sveltejs/kit": "^2.0.0",
1818
"@sveltejs/vite-plugin-svelte": "^3.0.0",
19+
"@types/cheerio": "^0.22.35",
1920
"@types/node": "^20.10.5",
2021
"@typescript-eslint/eslint-plugin": "^6.0.0",
2122
"@typescript-eslint/parser": "^6.0.0",
23+
"autoprefixer": "^10.4.16",
2224
"eslint": "^8.28.0",
2325
"eslint-config-prettier": "^9.1.0",
2426
"eslint-plugin-svelte": "^2.30.0",
27+
"postcss": "^8.4.32",
2528
"prettier": "^3.1.1",
2629
"prettier-plugin-svelte": "^3.1.2",
2730
"svelte": "^4.2.7",
2831
"svelte-check": "^3.6.0",
32+
"tailwindcss": "^3.4.0",
2933
"tslib": "^2.4.1",
3034
"typescript": "^5.0.0",
3135
"vite": "^5.0.3"
3236
},
33-
"type": "module"
37+
"type": "module",
38+
"dependencies": {
39+
"cheerio": "^1.0.0-rc.12"
40+
}
3441
}

src/website/postcss.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
}

src/website/src/app.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
h1 {
6+
@apply text-xl font-bold underline pb-2;
7+
}
8+
9+
h2 {
10+
@apply text-lg font-bold pb-1 pt-4;
11+
}
12+
13+
ul {
14+
@apply list-disc list-inside;
15+
}
16+
17+
a {
18+
@apply text-blue-500 underline;
19+
}
20+
21+
select {
22+
@apply border border-gray-300 py-1;
23+
}
24+
25+
button {
26+
@apply bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-4 rounded;
27+
}

src/website/src/routes/+layout.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
import '../app.css';
3+
</script>
4+
5+
<slot />

src/website/src/routes/+page.svelte

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
let total = 0;
77
let logFiles: string[] = [];
88
let finishedApiCalls = false;
9+
let lockChimes: any[] = [];
10+
let selectedUrl = '';
911
1012
async function add() {
1113
const response = await fetch('/api/dashboard', {
@@ -33,21 +35,51 @@
3335
return logFiles.logFiles;
3436
}
3537
38+
async function getLockChimes() {
39+
const response = await fetch('/api/lockChimes', {
40+
method: 'GET',
41+
headers: {
42+
'content-type': 'application/json'
43+
}
44+
});
45+
46+
const lockChimes = await response.json();
47+
console.log(lockChimes);
48+
49+
return lockChimes;
50+
}
51+
52+
const installLockChime = async () => {
53+
const response = await fetch('/api/lockChimes', {
54+
method: 'POST',
55+
body: JSON.stringify({ url: selectedUrl }),
56+
headers: {
57+
'content-type': 'application/json'
58+
}
59+
});
60+
61+
const installResponse = await response.json();
62+
console.log(installResponse);
63+
};
64+
3665
onMount(async () => {
3766
logFiles = await listLogFiles();
67+
lockChimes = await getLockChimes();
3868
console.log(logFiles);
3969
finishedApiCalls = true;
4070
});
4171
</script>
4272

73+
<div class="p-5">
74+
4375
<h1>node-teslausb</h1>
4476

4577
<h2>Log Files</h2>
4678
{#if finishedApiCalls == true}
4779
{#if logFiles.length == 0}
4880
<p>No log files found.</p>
4981
{:else}
50-
<ul>
82+
<ul class="list-disc">
5183
{#each logFiles as logFile}
5284
<li><a href={`/viewLog/${logFile}`}>{logFile}</a></li>
5385
{/each}
@@ -57,6 +89,27 @@
5789
<p>Loading...</p>
5890
{/if}
5991

92+
{#if finishedApiCalls}
93+
<h2>Lock Chimes</h2>
94+
{#if lockChimes.length == 0}
95+
<p>No lock chimes found.</p>
96+
{:else}
97+
<select bind:value={selectedUrl}>
98+
{#each lockChimes as lockChime}
99+
<option value={lockChime.url}>{lockChime.title}</option>
100+
{/each}
101+
</select>
102+
<button on:click={installLockChime}>Install</button>
103+
{/if}
104+
<div class="text-sm pt-1">
105+
Visit <a href="https://teslapro.hu/lockchimes/">https://teslapro.hu/lockchimes/</a> to listen to lock sounds before installing.
106+
<p class="pt-2">
107+
<i>Note: currently, installation will happen within 2 minutes if no data is being synced. Otherwise it will run once the sync finishes.</i>
108+
</p>
109+
</div>
110+
{/if}
111+
112+
</div>
60113
<!-- <p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
61114
62115
<input type="number" bind:value={a}> +
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { json } from '@sveltejs/kit';
2+
import cheerio from 'cheerio';
3+
import fs from 'fs/promises';
4+
import type { RequestEvent, RequestHandler } from '@sveltejs/kit';
5+
import { Readable } from 'stream';
6+
7+
interface LockChime {
8+
title: string;
9+
url: string;
10+
}
11+
12+
const getLockChimes = async (): Promise<LockChime[]> => {
13+
try {
14+
const url = 'https://teslapro.hu/lockchimes/';
15+
const html = await fetchHTML(url);
16+
return extractLockChimes(html);
17+
} catch (error) {
18+
console.error('Error fetching lock chimes:', error);
19+
return [];
20+
}
21+
};
22+
23+
const fetchHTML = async (url: string): Promise<string> => {
24+
const response = await fetch(url);
25+
if (!response.ok) {
26+
throw new Error(`Error fetching ${url}: ${response.statusText}`);
27+
}
28+
return await response.text();
29+
};
30+
31+
const extractLockChimes = (html: string): LockChime[] => {
32+
const $ = cheerio.load(html);
33+
const lockChimes: LockChime[] = [];
34+
35+
$('.card').each((i, element) => {
36+
const title = $(element).find('.card-title').text().trim();
37+
let url = $(element).find('a').attr('href') ?? '';
38+
if (url) {
39+
url = `https://teslapro.hu/lockchimes/${url}`;
40+
}
41+
42+
if (title && url) {
43+
lockChimes.push({ title, url });
44+
}
45+
});
46+
47+
return lockChimes;
48+
};
49+
50+
const downloadLockChime = async (url: string): Promise<string> => {
51+
const response = await fetch(url);
52+
if (!response.ok) {
53+
throw new Error(`Error fetching ${url}: ${response.statusText}`);
54+
}
55+
56+
const arrayBuffer = await response.arrayBuffer();
57+
const buffer = Buffer.from(arrayBuffer);
58+
const filename = 'LockChime.wav';
59+
const tmpPath = `/tmp/${filename}`;
60+
61+
await fs.writeFile(tmpPath, buffer);
62+
63+
return tmpPath;
64+
};
65+
66+
async function readableStreamToBuffer(readable: ReadableStream<Uint8Array>): Promise<Buffer> {
67+
const reader = readable.getReader();
68+
const chunks: Uint8Array[] = [];
69+
70+
while (true) {
71+
const { done, value } = await reader.read();
72+
if (done) break;
73+
chunks.push(value);
74+
}
75+
76+
return Buffer.concat(chunks);
77+
}
78+
79+
export async function GET({ request }) {
80+
const lockChimes = await getLockChimes();
81+
// const logNames = Object.keys(logNameToPathMapping);
82+
return json(lockChimes);
83+
}
84+
85+
export const POST: RequestHandler = async (event: RequestEvent) => {
86+
// Convert ReadableStream to Buffer
87+
const buffer = await readableStreamToBuffer(event.request.body!);
88+
89+
// Convert Buffer to string (assuming the content is text-based like JSON)
90+
const rawBody = buffer.toString();
91+
92+
// Parse the string as JSON (if the content type is JSON)
93+
const parsedBody = JSON.parse(rawBody);
94+
console.log("lockChimeUrl:",parsedBody)
95+
96+
await downloadLockChime(parsedBody.url);
97+
98+
return new Response(JSON.stringify({ message: `OK` }), {
99+
status: 200,
100+
headers: {
101+
'Content-Type': 'application/json'
102+
}
103+
});
104+
}
105+
106+
// Usage
107+
// console.log(lockChimes)

src/website/tailwind.config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/** @type {import('tailwindcss').Config} */
2+
export default {
3+
content: ['./src/**/*.{html,js,svelte,ts}'],
4+
theme: {
5+
extend: {},
6+
},
7+
plugins: [],
8+
}
9+

src/worker/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getRcloneConfig, rcloneCopyWithProgress } from './modules/rclone.js';
44
import { logWithTimestamp, errorWithTimestamp } from './modules/log.js';
55
import { restartWifi, checkIfArchiveIsReachable } from './modules/network.js';
66
import { mountTeslaCamAsReadOnly, unmountTeslaCam } from './modules/storage.js';
7+
import { checkLockChime } from './modules/lockChimes.js';
78

89
const config = {
910
archive: {
@@ -30,6 +31,8 @@ const processInterval = async () => {
3031

3132
// TODO: add a health check that checks - if on wifi, but no wifi clients, and cannot connect to source, or copy job has been running for 2+ hrs (once refactored to run 1 rclone job per folder), then reboot
3233

34+
await checkLockChime();
35+
3336
const archiveReachable = await checkIfArchiveIsReachable(config.archive.server);
3437
if (archiveReachable === false) {
3538
await restartWifi();

src/worker/modules/lockChimes.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { logWithTimestamp } from "./log";
2+
import { mountTeslaCamAsReadWrite, mountUsbDriveToHost, unmountTeslaCam, unmountUsbDriveFromHost } from "./storage";
3+
4+
export const checkLockChime = async () => {
5+
const configPath = '/tmp/LockChime.wav';
6+
if (fs.existsSync(configPath) === false) return
7+
8+
logWithTimestamp(`LockChime.wav found, installing...`)
9+
await installLockChime();
10+
11+
logWithTimestamp(`LockChime.wav installed, removing /tmp/LockChime.wav...`)
12+
await executeBashCommand(`rm /tmp/LockChime.wav`)
13+
14+
logWithTimestamp(`LockChime.wav removed.`)
15+
16+
}
17+
18+
const installLockChime = async () => {
19+
unmountTeslaCam();
20+
mountTeslaCamAsReadWrite();
21+
unmountUsbDriveFromHost();
22+
logWithTimestamp(`Copying LockChime.wav to USB drive`)
23+
await executeBashCommand(`cp /tmp/LockChime.wav /mnt/usb/TeslaCam/SentryClips/`)
24+
mountUsbDriveToHost();
25+
}

src/worker/modules/storage.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,30 @@ import { logWithTimestamp, errorWithTimestamp } from "./log.js"
22
import { executeBashCommand } from "./bash.js"
33

44
export const mountTeslaCamAsReadOnly = async () => {
5-
logWithTimestamp("Mounting TeslaCam")
5+
logWithTimestamp("Mounting TeslaCam as ro")
66
await executeBashCommand("sudo mount -o ro /vusb/TeslaCam /mnt/TeslaCam && systemctl daemon-reload")
77
}
88

9+
export const mountTeslaCamAsReadWrite = async () => {
10+
logWithTimestamp("Mounting TeslaCam as rw - this can corrupt data if also being written to the by the host")
11+
await executeBashCommand("sudo mount -o rw /vusb/TeslaCam /mnt/TeslaCam && systemctl daemon-reload")
12+
}
13+
914
export const unmountTeslaCam = async () => {
1015
logWithTimestamp("Unmounting TeslaCam")
1116
await executeBashCommand("sudo umount /mnt/TeslaCam && systemctl daemon-reload")
1217
}
1318

19+
export const unmountUsbDriveFromHost = async () => {
20+
logWithTimestamp("Unmounting USB drive from host")
21+
await executeBashCommand("sudo rmmod g_mass_storage")
22+
}
23+
24+
export const mountUsbDriveToHost = async () => {
25+
logWithTimestamp("Mounting USB drive to host")
26+
await executeBashCommand("sudo modprobe g_mass_storage file=/vusb/TeslaCam")
27+
}
28+
1429
// Not in use - leaving for now as it may be useful later
1530
// async function listFolderContents(folderPath, recursive = false) {
1631
// let entries = [];

0 commit comments

Comments
 (0)