Skip to content

Commit

Permalink
Minimum version checks (#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zoe authored Jul 19, 2024
1 parent 2f282d4 commit f7b6f6e
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 37 deletions.
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ words:
- cbar
- mbar
- quickboot
- websockets
- rgbw
- rgbww
- rrule
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@digital-alchemy/hass",
"repository": "https://github.com/Digital-Alchemy-TS/hass",
"homepage": "https://docs.digital-alchemy.app",
"version": "24.7.5",
"version": "24.7.6",
"scripts": {
"build": "rm -rf dist/; tsc",
"lint": "eslint src",
Expand Down Expand Up @@ -30,21 +30,23 @@
"dependencies": {
"dayjs": "^1.11.11",
"prom-client": "^15.1.2",
"semver": "^7.6.3",
"validator": "^13.12.0",
"ws": "^8.17.0"
},
"license": "MIT",
"devDependencies": {
"@cspell/eslint-plugin": "^8.8.4",
"@digital-alchemy/core": "^24.7.1",
"@digital-alchemy/synapse": "^24.6.6",
"@digital-alchemy/type-writer": "^24.6.6",
"@digital-alchemy/core": "^24.7.2",
"@digital-alchemy/synapse": "^24.7.1",
"@digital-alchemy/type-writer": "^24.7.2",
"@types/figlet": "^1.5.8",
"@types/jest": "^29.5.12",
"@types/js-yaml": "^4.0.9",
"@types/minimist": "^1.2.5",
"@types/mute-stream": "^0.0.4",
"@types/node": "^20.14.2",
"@types/semver": "^7.5.8",
"@types/uuid": "^9.0.8",
"@types/validator": "^13.11.10",
"@types/ws": "^8.5.10",
Expand Down
8 changes: 3 additions & 5 deletions src/extensions/config.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,9 @@ export function Configure({
config,
internal,
}: TServiceParams) {
/**
* Check for environment defined tokens provided by Home Assistant
*
* If available, override defaults to match
*/
lifecycle.onPreInit(() => {
// HASSIO_TOKEN provided by home assistant to addons
// SUPERVISOR_TOKEN used as alias elsewhere
const token = env.HASSIO_TOKEN || env.SUPERVISOR_TOKEN;
if (is.empty(token)) {
return;
Expand All @@ -44,6 +41,7 @@ export function Configure({
internal.boilerplate.configuration.set(
"hass",
"BASE_URL",
// don't go over the network
env.HASS_SERVER || "http://supervisor/core",
);
internal.boilerplate.configuration.set("hass", "TOKEN", token);
Expand Down
20 changes: 20 additions & 0 deletions src/extensions/fetch-api.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
UP,
} from "@digital-alchemy/core";
import dayjs, { Dayjs } from "dayjs";
import { exit } from "process";
import { lt } from "semver";

import {
ALL_SERVICE_DOMAINS,
Expand All @@ -20,6 +22,7 @@ import {
HassConfig,
HassServiceDTO,
HomeAssistantServerLogItem,
MIN_SUPPORTED_HASS_VERSION,
PICK_SERVICE,
PICK_SERVICE_PARAMETERS,
PostConfigPriorities,
Expand Down Expand Up @@ -51,6 +54,23 @@ export function FetchAPI({
fetcher.base_headers = { Authorization: `Bearer ${config.hass.TOKEN}` };
}, PostConfigPriorities.FETCH);

lifecycle.onBootstrap(async () => {
if (!config.hass.AUTO_CONNECT_SOCKET) {
// shorthand for is unit test right now
return;
}
const target = await hass.fetch.getConfig();
if (lt(target.version, MIN_SUPPORTED_HASS_VERSION)) {
logger.fatal(
{ target: target.version },
"minimum supported version of home assistant: %s",
MIN_SUPPORTED_HASS_VERSION,
);
exit();
}
logger.debug(`hass version %s`, target.version);
});

async function calendarSearch({
calendar,
start = dayjs(),
Expand Down
29 changes: 25 additions & 4 deletions src/hass.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,14 @@ export const LIB_HASS = CreateLibrary({
// no internal dependency ones first
priorityInit: ["fetch", "socket"],
services: {
/**
* home assistant areas
*/
area: Area,

/**
* home assistant backup interactions
*/
backup: Backup,

/**
Expand All @@ -134,44 +140,59 @@ export const LIB_HASS = CreateLibrary({
*/
configure: Configure,

/**
* device interactions
*/
device: Device,

/**
* retrieve and interact with home assistant entities
*/
entity: EntityManager,

/**
* Named event attachments
* named event attachments
*/
events: Events,

/**
* rest api commands
*/
fetch: FetchAPI,

/**
* floors, like groups of areas
*/
floor: Floor,

/**
* Search for entity ids in a type safe way
* search for entity ids in a type safe way
*/
idBy: IDByExtension,

/**
* home assistant label interactions
*/
label: Label,

/**
* Obtain references to entities
* obtain references to entities
*/
refBy: ReferenceExtension,

/**
* Interact with the home assistant registry
* interact with the home assistant registry
*/
registry: Registry,

/**
* websocket interface
*/
socket: WebsocketAPI,

/**
* zone interactions
*/
zone: Zone,
},
});
Expand Down
7 changes: 7 additions & 0 deletions src/helpers/constants.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ export enum HassSocketMessageTypes {
export const HOME_ASSISTANT_MODULE_CONFIGURATION =
"HOME_ASSISTANT_MODULE_CONFIGURATION";

/**
* Required for label support, which is an automatic process at boot
*
* Will not make feature optional to support older hass versions
* Update your stuff
*/
export const MIN_SUPPORTED_HASS_VERSION = "2024.4.0";
export const EARLY_ON_READY = 1;
export const ENTITY_REGISTRY_UPDATED = "ENTITY_REGISTRY_UPDATED";
export const AREA_REGISTRY_UPDATED = "AREA_REGISTRY_UPDATED";
Expand Down
13 changes: 11 additions & 2 deletions src/mock_assistant/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,20 @@ import {
} from "@digital-alchemy/core";

import { LIB_HASS } from "../../hass.module";
import { HassConfig } from "../../helpers";
import { LIB_MOCK_ASSISTANT } from "../mock-assistant.module";

function Rewire({ hass }: TServiceParams) {
jest
.spyOn(hass.fetch, "getConfig")
.mockImplementation(async () => ({ version: "2024.4.1" }) as HassConfig);
}
export const SILENT_BOOT = (
configuration: PartialConfiguration = {},
fixtures = false,
rewire = true,
): BootstrapOptions => ({
appendLibrary: fixtures ? LIB_MOCK_ASSISTANT : undefined,
appendService: rewire ? { Rewire } : undefined,
configuration,
// quiet time
customLogger: {
Expand Down Expand Up @@ -55,7 +62,9 @@ export const CreateTestRunner = <
}
return await UNIT_TESTING_APP.bootstrap({
appendLibrary: LIB_MOCK_ASSISTANT,
appendService: is.function(setup) ? { setup, test } : { test },
appendService: is.function(setup)
? { Rewire, setup, test }
: { Rewire, test },
configuration: {
boilerplate: { LOG_LEVEL: "error" },
hass: { MOCK_SOCKET: true },
Expand Down
87 changes: 80 additions & 7 deletions src/testing/fetch-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "@digital-alchemy/core";
import dayjs from "dayjs";

import { HassConfig } from "../helpers";
import {
CreateTestingApplication,
SILENT_BOOT,
Expand Down Expand Up @@ -186,6 +187,74 @@ describe("FetchAPI", () => {
);
});

// TODO: Need a way to make this pass without breaking all other tests
it.skip("exits for low version at boot", async () => {
expect.assertions(2);
let mock: jest.SpyInstance;
let exitSpy: jest.SpyInstance;
application = CreateTestingApplication({
Test({ hass }: TServiceParams) {
mock = jest
.spyOn(hass.fetch, "getConfig")
.mockImplementation(
async () => ({ version: "2024.1.0" }) as HassConfig,
);
exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {
throw new Error("EXPECTED TESTING ERROR");
});
},
});
try {
await application.bootstrap(
SILENT_BOOT({
hass: {
AUTO_CONNECT_SOCKET: false,
AUTO_SCAN_CALL_PROXY: false,
BASE_URL,
TOKEN,
},
}),
);
} finally {
expect(mock).toHaveBeenCalled();
expect(exitSpy).toHaveBeenCalled();
}
});

// TODO: Need a way to make this pass without breaking all other tests
it.skip("passes for valid version", async () => {
expect.assertions(2);
let mock: jest.SpyInstance;
let exitSpy: jest.SpyInstance;
application = CreateTestingApplication({
Test({ hass }: TServiceParams) {
mock = jest
.spyOn(hass.fetch, "getConfig")
.mockImplementation(
async () => ({ version: "2024.4.1" }) as HassConfig,
);
exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {
throw new Error("EXPECTED TESTING ERROR");
});
},
});
try {
await application.bootstrap(
SILENT_BOOT({
hass: {
AUTO_CONNECT_SOCKET: false,
AUTO_SCAN_CALL_PROXY: false,
BASE_URL,
TOKEN,
},
}),
);
} finally {
expect(mock).toHaveBeenCalled();
expect(exitSpy).not.toHaveBeenCalled();
}
});

it("should format fetchEntityCustomizations properly", async () => {
expect.assertions(1);
application = CreateTestingApplication({
Expand Down Expand Up @@ -351,14 +420,18 @@ describe("FetchAPI", () => {
},
});
await application.bootstrap(
SILENT_BOOT({
hass: {
AUTO_CONNECT_SOCKET: false,
AUTO_SCAN_CALL_PROXY: false,
BASE_URL,
TOKEN,
SILENT_BOOT(
{
hass: {
AUTO_CONNECT_SOCKET: false,
AUTO_SCAN_CALL_PROXY: false,
BASE_URL,
TOKEN,
},
},
}),
false,
false,
),
);
});

Expand Down
Loading

0 comments on commit f7b6f6e

Please sign in to comment.