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

Rooms based off area #7

Merged
merged 1 commit into from
Apr 27, 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 @@ -5,5 +5,6 @@ dictionaries: []
words:
- hass
- sonarjs
- mireds
ignoreWords: []
import: []
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"name": "@digital-alchemy/automation",
"repository": "https://github.com/Digital-Alchemy-TS/automation",
"homepage": "https://docs.digital-alchemy.app/Automation",
"version": "0.3.5",
"version": "0.3.6",
"scripts": {
"build": "rm -rf dist/; tsc",
"lint": "eslint src",
"test": "./scripts/test.sh",
"prepublishOnly": "npm run build",
"upgrade": "ncu -u; npm i"
"upgrade": "ncu -f '@digital-alchemy/*' -u; npm i"
},
"author": {
"url": "https://github.com/zoe-codez",
Expand All @@ -25,8 +25,8 @@
},
"license": "MIT",
"dependencies": {
"@digital-alchemy/core": "^0.3.11",
"@digital-alchemy/hass": "^0.3.9",
"@digital-alchemy/core": "^0.3.12",
"@digital-alchemy/hass": "^0.3.20",
"@digital-alchemy/synapse": "^0.3.5",
"dayjs": "^1.11.10",
"prom-client": "^15.1.1"
Expand Down
59 changes: 37 additions & 22 deletions src/extensions/aggressive-scenes.extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { each, is, TContext, TServiceParams } from "@digital-alchemy/core";
import { domain, ENTITY_STATE, PICK_ENTITY } from "@digital-alchemy/hass";
import {
domain,
ENTITY_STATE,
PICK_ENTITY,
PICK_FROM_AREA,
TAreaId,
} from "@digital-alchemy/hass";

import {
AGGRESSIVE_SCENES_ADJUSTMENT,
Expand All @@ -9,11 +15,11 @@ import {
SceneSwitchState,
} from "../helpers";

type TValidateOptions = {
type TValidateOptions<ROOM extends TAreaId> = {
context: TContext;
room: string;
name: string;
scene: RoomScene;
scene: RoomScene<ROOM>;
};

export function AggressiveScenes({
Expand All @@ -24,12 +30,14 @@ export function AggressiveScenes({
automation,
}: TServiceParams) {
// eslint-disable-next-line sonarjs/cognitive-complexity
async function manageSwitch(
entity: ENTITY_STATE<PICK_ENTITY<"switch">>,
scene: SceneDefinition,
) {
const entity_id = entity.entity_id as PICK_ENTITY<"switch">;
const expected = scene[entity_id] as SceneSwitchState;
async function manageSwitch<
ROOM extends TAreaId,
SCENE extends SceneDefinition<ROOM>,
>(entity: ENTITY_STATE<PICK_FROM_AREA<ROOM, "switch">>, scene: SCENE) {
const entity_id = entity.entity_id as PICK_FROM_AREA<ROOM, "switch">;
const expected = scene[
entity_id as Extract<keyof SCENE, PICK_FROM_AREA<ROOM, "switch">>
] as SceneSwitchState;
if (is.empty(expected)) {
// ??
return;
Expand All @@ -43,19 +51,23 @@ export function AggressiveScenes({
}
let performedUpdate = false;
if (entity.state !== expected.state) {
await matchSwitchToScene(entity, expected);
await matchSwitchToScene(
entity as ENTITY_STATE<PICK_FROM_AREA<ROOM, "switch">>,
expected,
);
performedUpdate = true;
}
if (performedUpdate) {
return;
}

if ("entity_id" in entity.attributes) {
const attributes = entity.attributes as { entity_id: PICK_ENTITY[] };
if ("entity_id" in attributes) {
// ? This is a group
const id = entity.attributes.entity_id;
const id = attributes.entity_id;
if (is.array(id) && !is.empty(id)) {
await each(
entity.attributes.entity_id as PICK_ENTITY<"switch">[],
attributes.entity_id as PICK_ENTITY<"switch">[],
async child_id => {
const child = hass.entity.byId(child_id);
if (!child) {
Expand All @@ -75,16 +87,20 @@ export function AggressiveScenes({
return;
}
if (child.state !== expected.state) {
await matchSwitchToScene(child, expected);
await matchSwitchToScene<ROOM>(
// @ts-expect-error wtf
child as ENTITY_STATE<PICK_FROM_AREA<ROOM, "switch">>,
expected,
);
}
},
);
}
}
}

async function matchSwitchToScene(
entity: ENTITY_STATE<PICK_ENTITY<"switch">>,
async function matchSwitchToScene<ROOM extends TAreaId>(
entity: ENTITY_STATE<PICK_FROM_AREA<ROOM, "switch">>,
expected: SceneSwitchState,
) {
const entity_id = entity.entity_id;
Expand All @@ -110,12 +126,12 @@ export function AggressiveScenes({
* - warnings
* - state changes
*/
async function validateRoomScene({
async function validateRoomScene<ROOM extends TAreaId>({
scene,
room,
name,
context,
}: TValidateOptions): Promise<void> {
}: TValidateOptions<ROOM>): Promise<void> {
if (
config.automation.AGGRESSIVE_SCENES === false ||
scene?.aggressive === false
Expand Down Expand Up @@ -160,10 +176,9 @@ export function AggressiveScenes({
);
return;
case "switch":
await manageSwitch(
entity as ENTITY_STATE<PICK_ENTITY<"switch">>,
scene.definition,
);
// @ts-expect-error wtf
const item = entity as ENTITY_STATE<PICK_FROM_AREA<ROOM, "switch">>;
await manageSwitch(item, scene.definition);
return;
default:
logger.debug(
Expand Down
26 changes: 14 additions & 12 deletions src/extensions/light-manager.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
ENTITY_STATE,
GenericEntityDTO,
PICK_ENTITY,
PICK_FROM_AREA,
TAreaId,
} from "@digital-alchemy/hass";

import { RoomDefinition } from "..";
Expand Down Expand Up @@ -77,11 +79,11 @@ export function LightManager({
*
* Same as RGB only, but will preferentially use color temp mode
*/
async function manageLight(
async function manageLight<ROOM extends TAreaId>(
entity: ENTITY_STATE<PICK_ENTITY<"light">>,
scene: SceneDefinition,
scene: SceneDefinition<ROOM>,
) {
const entity_id = entity.entity_id as PICK_ENTITY<"light">;
const entity_id = entity.entity_id as PICK_FROM_AREA<ROOM, "light">;
const expected = scene[entity_id] as SceneLightState;
if (is.empty(expected)) {
// ??
Expand Down Expand Up @@ -283,15 +285,15 @@ export function LightManager({
// Notice already being emitted from room extension
return [];
}
return Object.keys(room.currentSceneDefinition.definition).filter(
key => {
if (!is.domain(key, "light")) {
return false;
}
// TODO: Introduce additional checks for items like rgb color
return room.currentSceneDefinition.definition[key].state !== "off";
},
);
const keys = Object.keys(current) as (keyof typeof current)[];
return keys.filter(key => {
if (!is.domain(key, "light")) {
return false;
}
const entity = current[key] as { state: string };
// TODO: Introduce additional checks for items like rgb color
return entity.state !== "off";
});
}),
) as PICK_ENTITY<"light">[];

Expand Down
48 changes: 35 additions & 13 deletions src/extensions/room.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,20 @@ import {
TServiceParams,
VALUE,
} from "@digital-alchemy/core";
import { ALL_DOMAINS, PICK_ENTITY } from "@digital-alchemy/hass";
import {
ALL_DOMAINS,
PICK_ENTITY,
PICK_FROM_AREA,
TAreaId,
} from "@digital-alchemy/hass";
import { VirtualSensor } from "@digital-alchemy/synapse";

import { RoomConfiguration, RoomScene, SceneLightState } from "..";
import {
RoomConfiguration,
RoomScene,
SceneDefinition,
SceneLightState,
} from "..";

function toHassId<DOMAIN extends ALL_DOMAINS>(
domain: DOMAIN,
Expand All @@ -23,11 +33,15 @@ function toHassId<DOMAIN extends ALL_DOMAINS>(
return `${domain}.${name}` as PICK_ENTITY<DOMAIN>;
}

export type RoomDefinition<SCENES extends string = string> = {
export type RoomDefinition<
SCENES extends string = string,
ROOM extends TAreaId = TAreaId,
> = {
scene: SCENES;
currentSceneDefinition: RoomScene;
currentSceneDefinition: RoomScene<ROOM>;
currentSceneEntity: VirtualSensor<SCENES>;
sceneId: (scene: SCENES) => PICK_ENTITY<"scene">;
name: ROOM;
};
interface HasKelvin {
kelvin: number;
Expand All @@ -42,11 +56,11 @@ export function Room({
context: parentContext,
}: TServiceParams) {
// eslint-disable-next-line sonarjs/cognitive-complexity
return function <SCENES extends string>({
name,
return function <SCENES extends string, ROOM extends TAreaId>({
area: name,
context,
scenes,
}: RoomConfiguration<SCENES>): RoomDefinition<SCENES> {
}: RoomConfiguration<SCENES, ROOM>): RoomDefinition<SCENES> {
logger.info({ name }, `create room`);
const SCENE_LIST = Object.keys(scenes) as SCENES[];

Expand Down Expand Up @@ -107,7 +121,8 @@ export function Room({
if (!is.empty(target) && target !== "on") {
return false;
}
const current = (scenes[currentScene.state as SCENES] ?? {}) as RoomScene;
const current = (scenes[currentScene.state as SCENES] ??
{}) as RoomScene<ROOM>;
const definition = current.definition;
if (entity_id in definition) {
const state = definition[entity_id] as SceneLightState;
Expand All @@ -119,15 +134,18 @@ export function Room({
}

function dynamicProperties(sceneName: SCENES) {
const item = scenes[sceneName] as RoomScene;
const definition = item.definition as Record<
PICK_ENTITY<"light">,
SceneLightState
const { definition } = scenes[sceneName] as RoomScene<
ROOM,
SceneDefinition<ROOM>
>;
const entities = Object.keys(item.definition) as PICK_ENTITY<"light">[];
if (!is.object(definition)) {
return { lights: {}, scene: {} };
}
const entities = Object.keys(definition) as PICK_FROM_AREA<ROOM>[];
const kelvin = automation.circadian.getKelvin();
const list = entities
.map(name => {
// @ts-expect-error wtf
const value = definition[name] as SceneLightState;

if (is.domain(name, "switch")) {
Expand All @@ -142,6 +160,7 @@ export function Room({
return [name, { kelvin, ...value }];
})
.filter(i => !is.undefined(i));

return {
lights: Object.fromEntries(
list.filter(i => !is.undefined((i[VALUE] as HasKelvin).kelvin)),
Expand Down Expand Up @@ -220,6 +239,9 @@ export function Room({
return toHassId("scene", name, scene);
};
}
if (property === "name") {
return name;
}
if (property === "currentSceneEntity") {
return currentScene;
}
Expand Down
Loading
Loading