Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimum version checks #57

Merged
merged 5 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
HassConfig,
HassServiceDTO,
HomeAssistantServerLogItem,
MIN_SUPPORTED_HASS_VERSION,
PICK_SERVICE,
PICK_SERVICE_PARAMETERS,
PostConfigPriorities,
Expand Down Expand Up @@ -51,6 +54,23 @@
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(

Check warning on line 64 in src/extensions/fetch-api.extension.ts

View check run for this annotation

Codecov / codecov/patch

src/extensions/fetch-api.extension.ts#L64

Added line #L64 was not covered by tests
{ target: target.version },
"minimum supported version of home assistant: %s",
MIN_SUPPORTED_HASS_VERSION,
);
exit();

Check warning on line 69 in src/extensions/fetch-api.extension.ts

View check run for this annotation

Codecov / codecov/patch

src/extensions/fetch-api.extension.ts#L69

Added line #L69 was not covered by tests
}
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
Loading