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

Time functions #8

Merged
merged 4 commits into from
May 16, 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
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,7 @@ export function ExampleRoom({ automation, context }: TServiceParams) {

// check sun position
if (automation.solar.isBetween("dawn", "dusk")) {

// create some reference points with dayjs
const [PM530, NOW] = automation.utils.shortTime(["PM5:30", "NOW"]);
return NOW.isBefore(PM530);
return automation.time.isBefore("PM5:30")
}
return false;
},
Expand Down
49 changes: 26 additions & 23 deletions package-lock.json

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

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@digital-alchemy/automation",
"repository": "https://github.com/Digital-Alchemy-TS/automation",
"homepage": "https://docs.digital-alchemy.app/Automation",
"version": "0.3.6",
"version": "0.3.7",
"scripts": {
"build": "rm -rf dist/; tsc",
"lint": "eslint src",
Expand All @@ -25,8 +25,8 @@
},
"license": "MIT",
"dependencies": {
"@digital-alchemy/core": "^0.3.12",
"@digital-alchemy/hass": "^0.3.20",
"@digital-alchemy/core": "^0.3.15",
"@digital-alchemy/hass": "^0.3.25",
"@digital-alchemy/synapse": "^0.3.5",
"dayjs": "^1.11.10",
"prom-client": "^15.1.1"
Expand Down
6 changes: 3 additions & 3 deletions src/automation.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
Room,
SequenceWatcher,
SolarCalculator,
Time,
} from "./extensions";
import { Utils } from "./extensions/utils.extension";

export const LIB_AUTOMATION = CreateLibrary({
configuration: {
Expand Down Expand Up @@ -73,7 +73,7 @@ export const LIB_AUTOMATION = CreateLibrary({
depends: [LIB_HASS, LIB_SYNAPSE],
name: "automation",
// light depends circadian
priorityInit: ["utils", "circadian"],
priorityInit: ["time", "circadian"],
services: {
/**
* # Aggressive Scenes extension
Expand Down Expand Up @@ -117,7 +117,7 @@ export const LIB_AUTOMATION = CreateLibrary({
/**
* Helper functions
*/
utils: Utils,
time: Time,
},
});

Expand Down
5 changes: 4 additions & 1 deletion src/extensions/aggressive-scenes.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export function AggressiveScenes({
return;
}

const attributes = entity.attributes as { entity_id: PICK_ENTITY[] };
// TODO: FIXME
const attributes = entity.attributes as unknown as {
entity_id: PICK_ENTITY[];
};
if ("entity_id" in attributes) {
// ? This is a group
const id = attributes.entity_id;
Expand Down
1 change: 1 addition & 0 deletions src/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./managed-switch.extension";
export * from "./room.extension";
export * from "./sequence-matcher.extension";
export * from "./solar-calc.extension";
export * from "./time.extension";
40 changes: 23 additions & 17 deletions src/extensions/light-manager.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ByIdProxy,
ENTITY_STATE,
GenericEntityDTO,
HassEntityContext,
PICK_ENTITY,
PICK_FROM_AREA,
TAreaId,
Expand All @@ -27,22 +28,27 @@ import {
} from "../helpers";

type ColorModes = "color_temp" | "xy" | "brightness";
export type ColorLight = GenericEntityDTO<{
brightness: number;
color_mode: ColorModes;
color_temp: number;
color_temp_kelvin: number;
entity_id?: PICK_ENTITY<"light">[];
hs_color: [h: number, s: number];
max_color_temp_kelvin: number;
max_mireds: number;
min_color_temp_kelvin: number;
min_mireds: number;
rgb_color: [number, number, number];
supported_color_modes: ColorModes[];
supported_features: number;
xy_color: [x: number, y: number];
}>;
export type ColorLight = GenericEntityDTO<
{
brightness: number;
color_mode: ColorModes;
color_temp: number;
color_temp_kelvin: number;
entity_id?: PICK_ENTITY<"light">[];
hs_color: [h: number, s: number];
max_color_temp_kelvin: number;
max_mireds: number;
min_color_temp_kelvin: number;
min_mireds: number;
rgb_color: [number, number, number];
supported_color_modes: ColorModes[];
supported_features: number;
xy_color: [x: number, y: number];
},
string,
HassEntityContext,
"light"
>;
// const MAX_DIFFERENCE = 100;

type DiffList = {
Expand Down Expand Up @@ -293,7 +299,7 @@ export function LightManager({
const entity = current[key] as { state: string };
// TODO: Introduce additional checks for items like rgb color
return entity.state !== "off";
});
}) as PICK_ENTITY<"light">[];
}),
) as PICK_ENTITY<"light">[];

Expand Down
1 change: 0 additions & 1 deletion src/extensions/room.extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ export function 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 Down
109 changes: 86 additions & 23 deletions src/extensions/solar-calc.extension.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import {
CronExpression,
is,
TBlackHole,
TContext,
TServiceParams,
} from "@digital-alchemy/core";
import { HassConfig } from "@digital-alchemy/hass";
import dayjs, { Dayjs } from "dayjs";
import {
Duration,
DurationUnitsObjectType,
DurationUnitType,
} from "dayjs/plugin/duration";
import EventEmitter from "events";

import { calcSolNoon, calcSunriseSet } from "..";
Expand Down Expand Up @@ -44,14 +49,52 @@ const degreesBelowHorizon = {
twilight: 6,
};
const UNLIMITED = 0;
type Part<CHAR extends string> = `${number}${CHAR}` | "";
type ISO_8601_PARTIAL =
| `${Part<"H" | "h">}${Part<"M" | "m">}${Part<"S" | "s">}`
| "";

export type OffsetTypes =
| Duration
| number
| DurationUnitsObjectType
| ISO_8601_PARTIAL
| [quantity: number, unit: DurationUnitType];

type TOffset = OffsetTypes | (() => OffsetTypes);

type OnSolarEvent = {
label?: string;
/**
* **Any quantity may be negative**
*
* Value must be:
* - (`number`) `ms`
* - (`tuple`) [`quantity`, `unit`]
* - (`string`) `ISO 8601` duration string: `P(#Y)(#M)(#D)(T(#H)(#M)(#S))`
* - (`object`) mapping of units to quantities
* - (`Duration`) `dayjs.duration` object
* - (`function`) a function that returns any of the above
* ---
* Offset calculated at midnight & init
*/
offset?: TOffset;
eventName: SolarEvents;
exec: () => TBlackHole;
};

type SolarReference = Record<SolarEvents, Dayjs> & {
isBetween: (a: SolarEvents, b: SolarEvents) => boolean;
loaded: boolean;
onEvent: (options: OnSolarEvent) => TBlackHole;
};

/**
* Benefits from a persistent cache, like Redis
*/
export function SolarCalculator({
logger,
cache,
internal,
scheduler,
hass,
lifecycle,
Expand Down Expand Up @@ -190,35 +233,55 @@ export function SolarCalculator({
return now.isBetween(solarReference[a], solarReference[b]);
};

function getNextTime(eventName: SolarEvents, offset: TOffset, label: string) {
let duration: Duration;
// * if function, unwrap
if (is.function(offset)) {
offset = offset();
logger.trace({ eventName, label, offset }, `resolved offset`);
}
// * if tuple, resolve
if (is.array(offset)) {
const [amount, unit] = offset;
duration = dayjs.duration(amount, unit);
// * resolve objects, or capture Duration
} else if (is.object(offset)) {
duration = isDuration(offset)
? (offset as Duration)
: dayjs.duration(offset as DurationUnitsObjectType);
}
// * resolve from partial ISO 8601
if (is.string(offset)) {
duration = dayjs.duration(`PT${offset.toUpperCase()}`);
}
// * ms
if (is.number(offset)) {
duration = dayjs.duration(offset, "ms");
}
return duration
? solarReference[eventName].add(duration)
: solarReference[eventName];
}

solarReference.onEvent = ({
context,
eventName,
label,
exec,
offset,
}: OnSolarEvent) => {
event.on(eventName, async () => {
await internal.safeExec({
duration: undefined,
errors: undefined,
exec: async () => await exec(),
executions: undefined,
labels: { context, label },
});
scheduler.sliding({
exec: async () => await exec(),
label,
next: () => getNextTime(eventName, offset, label),
reset: CronExpression.EVERY_DAY_AT_MIDNIGHT,
});
};

return solarReference as SolarReference;
}

type OnSolarEvent = {
context: TContext;
label?: string;
eventName: SolarEvents;
exec: () => TBlackHole;
};

type SolarReference = Record<SolarEvents, Dayjs> & {
isBetween: (a: SolarEvents, b: SolarEvents) => boolean;
loaded: boolean;
onEvent: (options: OnSolarEvent) => TBlackHole;
};
function isDuration(
item: Duration | DurationUnitsObjectType,
): item is Duration {
return typeof item.days === "function";
}
Loading
Loading