Skip to content

Commit

Permalink
Typing improvements & tests (#31)
Browse files Browse the repository at this point in the history
* add badge for testing

* remove stale code

* test coverage

* more tests

* type fix

* testing

* logging

* wat

* trace

* wtf?!?!?

* test

* disable lint

* PLEASE?!

* what about this?

* rollback

* test coverage

* tests by chatgtp

* reformatting

* refactor type params

* example

* basic domain proof of life

* adding coverage for scene

* binary_sensor coverage

* bus transfer

* test coverage

* lint

* case fixing

* case fix

* bump
  • Loading branch information
Zoe authored Sep 6, 2024
1 parent 5cb6a66 commit 40bc5e1
Show file tree
Hide file tree
Showing 63 changed files with 1,756 additions and 1,125 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"dir": false,
"i": false,
"params": false,
"fn": false,
"props": false,
"ref": false,
"temp": false
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[![codecov](https://codecov.io/github/Digital-Alchemy-TS/synapse/graph/badge.svg?token=IBGLY3RY68)](https://codecov.io/github/Digital-Alchemy-TS/synapse)

---

## 📘 Description

Welcome to `@digital-alchemy/synapse`!
Expand All @@ -21,11 +25,10 @@ Then add to your application / library
```typescript
import { LIB_SYNAPSE } from "@digital-alchemy/synapse";
import { LIB_HASS } from "@digital-alchemy/hass";
import { LIB_FASTIFY } from "@digital-alchemy/fastify-extension";

// application
const MY_APP = CreateApplication({
libraries: [LIB_SYNAPSE, LIB_HASS, LIB_FASTIFY],
libraries: [LIB_SYNAPSE, LIB_HASS],
name: "home_automation",
})

Expand Down
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ words:
- utcnow
- zeroconf
- cobertura
- entitylocals
- autoincrement
- ssdp
ignoreWords: []
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
"name": "@digital-alchemy/synapse",
"repository": "https://github.com/Digital-Alchemy-TS/synapse",
"homepage": "https://docs.digital-alchemy.app/Synapse",
"version": "24.9.2",
"version": "24.9.3",
"scripts": {
"start:mock": "tsx src/mock/main.ts",
"build": "rm -rf dist; tsc",
"test": "./scripts/test.sh",
"lint": "eslint src",
"prepublishOnly": "npm run build",
"upgrade": "yarn up '@digital-alchemy/*'"
"upgrade": "yarn up '@digital-alchemy/*'",
"act": "act pull_request"
},
"author": {
"url": "https://github.com/zoe-codez",
Expand All @@ -35,7 +36,6 @@
"devDependencies": {
"@cspell/eslint-plugin": "^8.8.1",
"@digital-alchemy/core": "^24.8.4",
"@digital-alchemy/fastify-extension": "^24.8.1",
"@digital-alchemy/hass": "^24.8.3",
"@types/better-sqlite3": "^7.6.10",
"@types/jest": "^29.5.12",
Expand Down
3 changes: 1 addition & 2 deletions scripts/test.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/bin/bash
rm jest_sqlite.db
NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules" npx jest --pass-with-no-tests "$1"
NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules" npx jest --pass-with-no-tests --runInBand "$1"
rm jest_sqlite.db
58 changes: 12 additions & 46 deletions src/extensions/configure.extension.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,8 @@
import { is, MINUTE, sleep, TServiceParams } from "@digital-alchemy/core";
import { is, TServiceParams } from "@digital-alchemy/core";
import { createHash } from "crypto";
import { hostname, networkInterfaces, userInfo } from "os";
import { hostname, userInfo } from "os";

const EXTRA_EARLY = 1000;

function getLocalIPAddress(): string | undefined {
const interfaces = networkInterfaces();
for (const interfaceName in interfaces) {
const addresses = interfaces[interfaceName];
if (addresses) {
for (const addressInfo of addresses) {
if (addressInfo.family === "IPv4" && !addressInfo.internal) {
return addressInfo.address;
}
}
}
}
return undefined;
}

export function Configure({
lifecycle,
config,
logger,
internal,
hass,
fastify,
synapse,
}: TServiceParams) {
export function Configure({ lifecycle, config, logger, internal, hass, synapse }: TServiceParams) {
let extensionInstalled = false;

function uniqueProperties(): string[] {
Expand Down Expand Up @@ -60,45 +35,36 @@ export function Configure({
}
});

// Mental note:
// This needs to happen in the `onPostConfig` step because the fastify port may change
lifecycle.onPostConfig(() => {
if (!is.undefined(fastify) && is.empty(config.synapse.METADATA_HOST)) {
const METADATA_HOST = `${getLocalIPAddress()}:${config.fastify.PORT}`;
logger.debug({ METADATA_HOST, name: "onPostConfig" }, `updating [METADATA_HOST]`);
internal.boilerplate.configuration.set("synapse", "METADATA_HOST", METADATA_HOST);
}
}, EXTRA_EARLY);

/**
* keep bothering user until they install the extension or remove the lib
* kinda pointless otherwise
*/
async function doCheck() {
if (!config.hass.AUTO_SCAN_CALL_PROXY) {
async function checkInstallState() {
if (config.synapse.ASSUME_INSTALLED) {
extensionInstalled = true;
return;
return true;
}
const hassConfig = await hass.fetch.getConfig();
const installed = hassConfig.components.some(i => i.startsWith("synapse"));
if (installed) {
logger.debug("extension is installed!");
extensionInstalled = true;
return;
return true;
}
logger.error(`synapse extension is not installed`);
await sleep(MINUTE);
return doCheck();
// retry
return false;
}

// hass.events.
// make sure it doesn't accidentally get attached to lifecycle
lifecycle.onBootstrap(() => setImmediate(async () => await doCheck()));
lifecycle.onBootstrap(async () => await synapse.configure.checkInstallState());

lifecycle.onReady(() => {
if (!synapse.configure.isRegistered()) {
logger.warn({ name: "onReady" }, `application is not registered in hass`);
}
});

return { isRegistered };
return { checkInstallState, isRegistered };
}
28 changes: 25 additions & 3 deletions src/extensions/device.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,31 @@ import { HassDeviceMetadata, md5ToUUID, TSynapseDeviceId } from "../helpers";

const host = hostname();

export function DeviceExtension({ config, lifecycle, logger, internal }: TServiceParams) {
export function DeviceExtension({ config, lifecycle, logger, internal, synapse }: TServiceParams) {
let synapseVersion: string;
const DEVICE_REGISTRY = new Map<string, HassDeviceMetadata>();

lifecycle.onPostConfig(() => {
synapseVersion = synapse.device.loadVersion();
});

function loadVersion(): string {
if (!is.empty(synapseVersion)) {
return synapseVersion;
}
const file = join(__dirname, "..", "..", "package.json");
if (existsSync(file)) {
logger.trace("loading package");
try {
const contents = readFileSync(file, "utf8");
const data = JSON.parse(contents) as { version: string };
synapseVersion = data?.version;
return data?.version;
} catch (error) {
logger.error(error);
}
}
});
return undefined;
}

return {
getInfo(): HassDeviceMetadata {
Expand All @@ -36,6 +44,7 @@ export function DeviceExtension({ config, lifecycle, logger, internal }: TServic
...config.synapse.METADATA,
};
},

/**
* Create a stable UUID to uniquely identify this app.
*
Expand All @@ -54,16 +63,29 @@ export function DeviceExtension({ config, lifecycle, logger, internal }: TServic
.digest("hex"),
);
},

list() {
return [...DEVICE_REGISTRY.keys()].map(unique_id => ({
...DEVICE_REGISTRY.get(unique_id),
hub_id: config.synapse.METADATA_UNIQUE_ID,
unique_id,
}));
},

/**
* override the `sw_version`
*
* normally loads version from package.json
*/
loadVersion,

register(id: string, data: HassDeviceMetadata): TSynapseDeviceId {
DEVICE_REGISTRY.set(id, data);
return id as TSynapseDeviceId;
},

setVersion(version: string) {
synapseVersion = version;
},
};
}
1 change: 0 additions & 1 deletion src/extensions/discovery.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export function DiscoveryExtension({
const APP_METADATA = () => ({
app: internal.boot.application.name,
device: synapse.device.getInfo(),
host: config.synapse.METADATA_HOST,
hostname: hostname(),
secondary_devices: synapse.device.list(),
title: config.synapse.METADATA_TITLE,
Expand Down
8 changes: 4 additions & 4 deletions src/extensions/domains/alarm-control-panel.extension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TServiceParams } from "@digital-alchemy/core";

import { AddEntityOptions, SettableConfiguration } from "../..";
import { AddEntityOptions, BasicAddParams, SettableConfiguration } from "../..";

export type AlarmControlPanelStates =
| "disarmed"
Expand Down Expand Up @@ -73,14 +73,14 @@ export function VirtualAlarmControlPanel({ context, synapse }: TServiceParams) {
],
});

return function <LOCALS extends object = object, ATTRIBUTES extends object = object>({
return function <PARAMS extends BasicAddParams>({
managed = true,
...options
}: AddEntityOptions<
AlarmControlPanelConfiguration,
AlarmControlPanelEvents,
ATTRIBUTES,
LOCALS
PARAMS["attributes"],
PARAMS["locals"]
>) {
const entity = generate.addEntity(options);
if (managed) {
Expand Down
11 changes: 8 additions & 3 deletions src/extensions/domains/binary-sensor.extension.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TServiceParams } from "@digital-alchemy/core";
import { BinarySensorDeviceClass } from "@digital-alchemy/hass";

import { AddEntityOptions, SettableConfiguration } from "../..";
import { AddEntityOptions, BasicAddParams, SettableConfiguration } from "../..";

export type BinarySensorConfiguration = {
/**
Expand All @@ -26,7 +26,12 @@ export function VirtualBinarySensor({ context, synapse }: TServiceParams) {
load_config_keys: ["device_class", "is_on"],
});

return <LOCALS extends object = object, ATTRIBUTES extends object = object>(
options: AddEntityOptions<BinarySensorConfiguration, BinarySensorEvents, ATTRIBUTES, LOCALS>,
return <PARAMS extends BasicAddParams>(
options: AddEntityOptions<
BinarySensorConfiguration,
BinarySensorEvents,
PARAMS["attributes"],
PARAMS["locals"]
>,
) => generate.addEntity(options);
}
11 changes: 8 additions & 3 deletions src/extensions/domains/button.extension.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TServiceParams } from "@digital-alchemy/core";
import { ButtonDeviceClass } from "@digital-alchemy/hass";

import { AddEntityOptions } from "../..";
import { AddEntityOptions, BasicAddParams } from "../..";

export type ButtonConfiguration = {
device_class?: `${ButtonDeviceClass}`;
Expand All @@ -21,7 +21,12 @@ export function VirtualButton({ context, synapse }: TServiceParams) {
load_config_keys: ["device_class"],
});

return <LOCALS extends object = object, ATTRIBUTES extends object = object>(
options: AddEntityOptions<ButtonConfiguration, ButtonEvents, ATTRIBUTES, LOCALS>,
return <PARAMS extends BasicAddParams>(
options: AddEntityOptions<
ButtonConfiguration,
ButtonEvents,
PARAMS["attributes"],
PARAMS["locals"]
>,
) => generate.addEntity(options);
}
11 changes: 8 additions & 3 deletions src/extensions/domains/camera.extension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TServiceParams } from "@digital-alchemy/core";

import { AddEntityOptions, SettableConfiguration } from "../..";
import { AddEntityOptions, BasicAddParams, SettableConfiguration } from "../..";

export type CameraConfiguration = {
/**
Expand Down Expand Up @@ -77,7 +77,12 @@ export function VirtualCamera({ context, synapse }: TServiceParams) {
],
});

return <LOCALS extends object = object, ATTRIBUTES extends object = object>(
options: AddEntityOptions<CameraConfiguration, CameraEvents, ATTRIBUTES, LOCALS>,
return <PARAMS extends BasicAddParams>(
options: AddEntityOptions<
CameraConfiguration,
CameraEvents,
PARAMS["attributes"],
PARAMS["locals"]
>,
) => generate.addEntity(options);
}
11 changes: 8 additions & 3 deletions src/extensions/domains/climate.extension.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TServiceParams } from "@digital-alchemy/core";
import { HVACAction, HVACMode } from "@digital-alchemy/hass";

import { AddEntityOptions, SettableConfiguration } from "../..";
import { AddEntityOptions, BasicAddParams, SettableConfiguration } from "../..";

export type ClimateConfiguration<
PRESET_MODES extends string = string,
Expand Down Expand Up @@ -170,7 +170,12 @@ export function VirtualClimate({ context, synapse }: TServiceParams) {
],
});

return <LOCALS extends object = object, ATTRIBUTES extends object = object>(
options: AddEntityOptions<ClimateConfiguration, ClimateEvents, ATTRIBUTES, LOCALS>,
return <PARAMS extends BasicAddParams>(
options: AddEntityOptions<
ClimateConfiguration,
ClimateEvents,
PARAMS["attributes"],
PARAMS["locals"]
>,
) => generate.addEntity(options);
}
11 changes: 8 additions & 3 deletions src/extensions/domains/cover.extension.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TServiceParams } from "@digital-alchemy/core";
import { CoverDeviceClass } from "@digital-alchemy/hass";

import { AddEntityOptions, SettableConfiguration } from "../..";
import { AddEntityOptions, BasicAddParams, SettableConfiguration } from "../..";

export type CoverConfiguration = {
/**
Expand Down Expand Up @@ -79,7 +79,12 @@ export function VirtualCover({ context, synapse }: TServiceParams) {
],
});

return <LOCALS extends object = object, ATTRIBUTES extends object = object>(
options: AddEntityOptions<CoverConfiguration, CoverEvents, ATTRIBUTES, LOCALS>,
return <PARAMS extends BasicAddParams>(
options: AddEntityOptions<
CoverConfiguration,
CoverEvents,
PARAMS["attributes"],
PARAMS["locals"]
>,
) => generate.addEntity(options);
}
Loading

0 comments on commit 40bc5e1

Please sign in to comment.