-
Notifications
You must be signed in to change notification settings - Fork 15
/
helpers.ts
203 lines (164 loc) · 8.72 KB
/
helpers.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import { AttributeConfig, TimerBarConfig, HassEntity, Mode, TimerBarEntityConfig } from "./types";
import { durationToSeconds, formatTime, HomeAssistant } from "custom-card-helpers";
export const MIN_SYNC_ERROR = 500; // Allow local and HA times to be 500ms different
export const MAX_FIX_SYNC_ERROR = 60000; // Allow sync fixes of up to 1hr
/** Date.now(), but with a specified correction in ms. */
function now(correction: number) {
return Date.now() + correction
}
export function tryDurationToSeconds(duration: string, field: string) {
try {
const seconds = durationToSeconds(duration);
if (isNaN(seconds)) throw new Error(`Error parsing ${field} ${duration}: check it matches the format 0:10:00`);
return seconds;
} catch (e) {
throw new Error(`Could not convert ${field}: ${duration} is not of format 0:10:00. If you are passing in a number, specify the units property.`);
}
}
export function usesLastChanged(hass: HomeAssistant, config: TimerBarConfig, stateObj: HassEntity) {
const duration = timeAttribute(hass, stateObj, config.duration);
const remain_time = timeAttribute(hass, stateObj, config.remain_time);
const start_time = attribute(hass, stateObj, config.start_time);
const end_time = attribute(hass, stateObj, config.end_time);
// Last changed is needed if at least 2 of duration, start time, and end time are undefined.
return (!duration && !end_time) || (!duration && !start_time) || (!duration && !remain_time) || (!end_time && !start_time);
}
// (duration OR start + end)
// AND
// (start + duration OR end)
// (duration + start + duration) OR (duration + end) OR (start + end + start) OR (start + end + end)
// (start + duration) OR (end + duration) OR (start + end) OR (start + end)
// (start + duration) OR (end + duration) OR (start + end)
/** Find the duration of the timer. */
export function findDuration(hass: HomeAssistant, config: TimerBarConfig, stateObj: HassEntity) {
const duration = timeAttribute(hass, stateObj, config.duration);
if (duration) return duration;
const start_time = attribute(hass, stateObj, config.start_time);
const end_time = attribute(hass, stateObj, config.end_time);
if (start_time && end_time) return (Date.parse(end_time) - Date.parse(start_time)) / 1000;
if (end_time) return (Date.parse(end_time) - Date.parse(stateObj.last_changed)) / 1000;
return null;
}
/** Calculate the most accurate estimate of time remaining for the timer. */
export const timerTimeRemaining = (hass: HomeAssistant, config: TimerBarConfig, stateObj: HassEntity, correction: number): undefined | number => {
const madeActive = Date.parse(stateObj.last_changed);
if (stateObj.attributes.remaining) { // For Home Assistant timers
let timeRemaining = tryDurationToSeconds(stateObj.attributes.remaining, 'remaining');
if (isState(stateObj, config.active_state!, config)) {
// Why timeRemaining and not duration?
timeRemaining = Math.max(timeRemaining - (now(correction) - madeActive) / 1000, 0);
}
return timeRemaining;
}
const end_time = attribute(hass, stateObj, config.end_time!);
if (end_time) // For OpenSprinkler timers + others
return (Date.parse(end_time) - now(correction)) / 1000;
const start_time = attribute(hass, stateObj, config.start_time);
const duration = timeAttribute(hass, stateObj, config.duration);
if (start_time && duration)
return (Date.parse(start_time) - now(correction)) / 1000 + duration;
// Second-to-last fallback: remain time attribute
const remain_time = timeAttribute(hass, stateObj, config.remain_time);
if (remain_time != undefined) {
return remain_time
}
// Final fallback: assume madeActive is the start time
if (duration)
return (madeActive - now(correction)) / 1000 + duration;
return undefined;
};
/** Calculate what percent of the timer's duration has passed. */
export const timerTimePercent = (hass: HomeAssistant, config: TimerBarConfig, stateObj: HassEntity, correction: number): undefined | number => {
const remaining = timerTimeRemaining(hass, config, stateObj, correction);
const duration = findDuration(hass, config, stateObj);
if (!duration || !remaining) return undefined;
return (duration - Math.floor(remaining)) / duration * 100;
};
export const formatStartTime = (stateObj: HassEntity) => {
const start = new Date(stateObj.attributes.start_time);
const lang = JSON.parse(localStorage.getItem('selectedLanguage') || '"en"') || 'en';
return formatTime(start, lang);
}
export const isState = (stateObj: HassEntity | undefined, checkState: string | string[], config: TimerBarConfig) => {
if (!stateObj) return false;
const state = config.state_attribute ? stateObj.attributes[config.state_attribute] : stateObj.state;
if (typeof checkState === 'string') return state === checkState;
return checkState.includes(state);
}
function nullifyState(value: string) {
if (value == "unavailable") return null
return value
}
export const attribute = (hass: HomeAssistant, stateObj: HassEntity, attrib: AttributeConfig | undefined) => {
if (!attrib) throw new Error('One of duration, remain_time, start_time, or end_time was not fully specified. Make sure you set entity, fixed, or attribute');
if ('fixed' in attrib) return attrib.fixed;
if ('script' in attrib) return hass.states[attrib.script].attributes['last_action']?.split('delay ')[1];
if ('entity' in attrib) {
// Two cases: entity + attribute or entity + state
if ('attribute' in attrib) return hass.states[attrib.entity].attributes[attrib.attribute]
return nullifyState(hass.states[attrib.entity].state);
}
if ('state' in attrib) return nullifyState(stateObj.state);
return stateObj.attributes[attrib.attribute];
}
const timeAttribute = (hass: HomeAssistant, stateObj: HassEntity, attrib: AttributeConfig | undefined) => {
const duration = attribute(hass, stateObj, attrib);
if (!duration) return duration;
if (attrib!.units === 'hours' || attrib!.units === 'minutes' || attrib!.units === 'seconds') {
const numeric = parseFloat(duration);
if (isNaN(numeric)) throw new Error(`Expected duration ${duration} to be a number since units is ${attrib!.units}`);
if (attrib!.units == 'hours') return numeric * 3600;
if (attrib!.units == 'minutes') return numeric * 60;
if (attrib!.units == 'seconds') return numeric * 1;
}
return tryDurationToSeconds(duration, 'duration');
}
export function autoMode(hass: HomeAssistant, config: TimerBarEntityConfig, correction: number): Mode | undefined {
// Disable if the last modified date is used and there is no end time
// Otherwise, auto mode might be enabled when it's not supposed to be!
const state = hass.states[config.entity!];
const end_time = attribute(hass, state, config.end_time);
if (usesLastChanged(hass, config, state) && !end_time) return undefined;
// Auto mode is not capable of determining whether the entity is paused or waiting
const stMode = stateMode(hass, config, correction);
if (stMode === 'pause' || stMode === 'waiting') return undefined;
const duration = findDuration(hass, config, state);
const remaining = timerTimeRemaining(hass, config, state, correction);
if (!duration || !remaining) return undefined;
if (remaining >= 0 && remaining <= duration + MIN_SYNC_ERROR) return 'active';
return undefined;
}
export function stateMode(hass: HomeAssistant, config: TimerBarEntityConfig, correction: number): Mode {
const state = hass.states[config.entity!];
if (isState(state, config.active_state!, config) && (timerTimeRemaining(hass, config, state, correction)||0) > 0) return 'active';
if (isState(state, config.pause_state!, config)) return 'pause';
if (isState(state, config.waiting_state!, config)) return 'waiting';
return 'idle';
}
export function findMode(hass: HomeAssistant, config: TimerBarEntityConfig, correction: number): Mode {
if (config.guess_mode) return autoMode(hass, config, correction)|| stateMode(hass, config, correction);
return stateMode(hass, config, correction);
}
/** Returns an array of entities referenced by the config.
* Useful for state updates
**/
export function gatherEntitiesFromConfig(config: TimerBarEntityConfig): string[] {
const entities: string[] = []
const addMaybe = (s: string|undefined) => s && entities.push(s)
const addMaybeAttr = (c: AttributeConfig|undefined) => {
if (c && 'entity' in c) entities.push(c.entity)
if (c && 'script' in c) entities.push(c.script)
}
addMaybe(config.entity)
addMaybeAttr(config.duration)
addMaybeAttr(config.remain_time)
addMaybeAttr(config.start_time)
addMaybeAttr(config.end_time)
return entities
}
export function haveEntitiesChanged(entities: string[], oldHass: HomeAssistant, newHass: HomeAssistant) {
for (const entity of entities) {
if (oldHass.states[entity] != newHass.states[entity]) return true
}
return false
}