Skip to content

Commit

Permalink
feat: new/better mapping composeable
Browse files Browse the repository at this point in the history
  • Loading branch information
TheNoim committed Jan 18, 2023
1 parent c3e2fa5 commit 935e850
Show file tree
Hide file tree
Showing 8 changed files with 515 additions and 5 deletions.
23 changes: 23 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"request": "launch",
"name": "Launch Program",
"type": "node",
"program": "${workspaceFolder}/test-modules/script.testlab.ts",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "/opt/homebrew/bin/deno",
"runtimeArgs": [
"run",
"--unstable",
"--inspect-wait",
"--allow-all"
],
"attachSimplePort": 9229
}
]
}
211 changes: 211 additions & 0 deletions reactive_home/src/composeables/useLightMapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { UseNewLightEntity } from "./useNewLight.ts";
import { UseNewBooleanEntity } from "./useNewBoolean.ts";
import {
watch,
unref,
parseDuration as parse,
computed,
useNow,
subMilliseconds,
} from "../dep.ts";
import type { MaybeRef } from "../lib/types.ts";
import type { HassEntity, Ref } from "../dep.ts";

export function useLightMapping({
entity,
expectedValue,
expectedBrightness,
isDisabled,
isDisabledBrightness,
debug,
autoEnableTime,
autoEnableTimeBrightness,
}: UseLightMappingOptions) {
watch(entity, (newEntityState, oldEntityState) => {
if (newEntityState.value !== unref(expectedValue) && isDisabled) {
if (debug) {
console.log(`automation_toggle(${newEntityState.entity_id}): value`);
}
isDisabled.value = true;
}

if (
expectedBrightness &&
newEntityState.value &&
isDisabledBrightness &&
unref(expectedBrightness) !== newEntityState.brightness &&
/** Skip initial value, because it might be different. Let it sync first */
oldEntityState.value
) {
if (debug) {
console.log(
`automation_toggle(${newEntityState.entity_id}): brightness`
);
}
isDisabledBrightness.value = true;
}
});

const autoEnableTimeParsed = parseAutoEnableTimeFactory(autoEnableTime);

const autoEnableTimeBrightnessParsed = parseAutoEnableTimeFactory(
autoEnableTimeBrightness
);

const now = useNow({ interval: 1000 });

const shouldReEnableSince = shouldReEnableSinceFactory(
autoEnableTimeParsed,
now,
isDisabled
);

const shouldReEnableSinceBrightness = shouldReEnableSinceFactory(
autoEnableTimeBrightnessParsed,
now,
isDisabledBrightness
);

watch(
[shouldReEnableSince, shouldReEnableSinceBrightness],
([newShouldReEnableSince, newShouldReEnableSinceBrightness]) => {
if (newShouldReEnableSince >= 0 && isDisabled) {
if (debug) {
console.log(
`isDisabled(${entity.entity_id}): reset time=${newShouldReEnableSince}`
);
}
isDisabled.value = false;
}
if (newShouldReEnableSinceBrightness >= 0 && isDisabledBrightness) {
if (debug) {
console.log(
`isDisabledBrightness(${entity.entity_id}): reset time=${newShouldReEnableSinceBrightness}`
);
}
isDisabledBrightness.value = false;
}
},
{ immediate: true }
);

const valueTimeSinceUnsync = computed(() => {
if (isDisabled?.value) {
return -1;
}
if (entity.value !== unref(expectedValue)) {
return now.value.getTime() - entity.lastChanged.getTime();
}
return -1;
});

const brightnessTimeSinceUnsync = computed(() => {
if (isDisabledBrightness?.value) {
return -1;
}
if (
entity.value &&
expectedBrightness &&
entity.brightness !== unref(expectedBrightness)
) {
return now.value.getTime() - entity.lastChanged.getTime();
}
return -1;
});

watch(
[valueTimeSinceUnsync, brightnessTimeSinceUnsync],
([newValueTime, newBrightnessTime]) => {
if (newValueTime >= 0) {
if (debug) {
console.log(
`out_of_sync(${
entity.entity_id
}): value time=${newValueTime} expected=${unref(
expectedValue
)} current=${entity.value}`
);
}
entity.value = unref(expectedValue);
}

const expectedBrightnessValue = unref(expectedBrightness);

if (newBrightnessTime >= 0 && expectedBrightnessValue) {
if (debug) {
console.log(
`out_of_sync(${entity.entity_id}): brightness time=${newBrightnessTime} expected=${expectedBrightnessValue} current=${entity.brightness}`
);
}
entity.brightness = expectedBrightnessValue;
}
}
);
}

export type UseLightMappingOptions = {
entity: UseNewLightEntity;

expectedValue: MaybeRef<boolean>;
expectedBrightness?: MaybeRef<number>;

isDisabled?: UseNewBooleanEntity;
isDisabledBrightness?: UseNewBooleanEntity;

autoEnableTime?: MaybeRef<number | HassEntity | string>;
autoEnableTimeBrightness?: MaybeRef<number | HassEntity | string>;

debug?: boolean;
};

export function parseAutoEnableTimeFactory(
input?: MaybeRef<number | HassEntity | string>
) {
return computed(() => {
if (!input) {
return 15 * 60 * 1000;
}
let value = unref(input);
if (typeof value === "number") {
return value;
}
if (typeof value === "object") {
value = value.state;
}
console.log({ value, input });
return parse(value, "ms");
});
}

/**
* Calculate time since `entity` requires a reset
* @param resetTimeSource Time in ms after which the entity should reset
* @param nowSource useNow() source
* @param entity Entity to reset
* @returns Time since the entity requires a reset
*/
export function shouldReEnableSinceFactory(
resetTimeSource: Ref<number>,
nowSource: Ref<Date>,
entity?: UseNewBooleanEntity
) {
return computed(() => {
if (!entity || !entity.value) {
return -1;
}

const now = unref(nowSource);

const resetTime = unref(resetTimeSource);

const lastChanged = entity.lastChanged;

const diffDate = subMilliseconds(now, resetTime);

if (lastChanged < diffDate) {
return diffDate.getTime() - lastChanged.getTime();
}

return -1;
});
}
95 changes: 95 additions & 0 deletions reactive_home/src/composeables/useNewBoolean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type { FullfilledUseState } from "./useState.ts";
import {
useDebounceFn,
ref,
watch,
watchPausable,
extendRef,
computed,
} from "../dep.ts";
import type { MessageBase, HassEntity, UnwrapNestedRefs } from "../dep.ts";
import { connection } from "../hass/connection.ts";
import { stringBoolToBool } from "../lib/util.ts";

export function useNewBoolean(state: FullfilledUseState, debug = false) {
const skipContexts: string[] = [];

const updateHASSState = useDebounceFn(async (newState: boolean) => {
const entity = state.value;

const payload = {
type: "call_service",
domain: entity.entity_id.split(".").at(0),
service: `turn_${newState ? "on" : "off"}`,
target: {
entity_id: entity.entity_id,
},
} satisfies MessageBase;

if (debug) {
console.log(`call_service(${entity.entity_id}): `, payload);
}

const result: { context: HassEntity["context"] } =
await connection.sendMessagePromise(payload);

if (result?.context?.id) {
skipContexts.push(result.context.id);
}
}, 50);

const localValue = ref(stringBoolToBool(state.value.state));

const lastChanged = ref(state.value.last_changed);

console.log("init", {
value: localValue.value,
lastChanged: lastChanged.value,
entity: state.value.entity_id,
});

watch(localValue, (newLocalValue) => {
console.log({ newLocalValue, entity: state.value.entity_id });
});

// Local state changes
const { pause, resume } = watchPausable(
localValue,
(newLocalValue: typeof localValue.value) => {
updateHASSState(newLocalValue);
}
);

resume();

// Incoming state changes from hass
watch(
() => state.value,
(newEntityState) => {
const contextIndex = skipContexts.findIndex(
(value) => value === newEntityState.context.id
);

if (contextIndex > -1) {
skipContexts.splice(contextIndex, 1);
return;
}
pause();
localValue.value = stringBoolToBool(newEntityState.state);
lastChanged.value = newEntityState.last_changed;
resume();
}
);

const extendObject = {
lastChanged: computed(() => new Date(lastChanged.value)),
getOriginal() {
return localValue;
},
};

return extendRef(localValue, extendObject) as typeof localValue &
UnwrapNestedRefs<typeof extendObject>;
}

export type UseNewBooleanEntity = ReturnType<typeof useNewBoolean>;
Loading

0 comments on commit 935e850

Please sign in to comment.