Initial release — Dibby Wemo Manager v2.0.0
Desktop (Electron/Windows): device dashboard, DWM scheduling engine, native firmware rules editor, Windows background service, web remote, sunrise/sunset support. Homebridge plugin (homebridge-dibby-wemo v1.0.0): HomeKit switches for all local Wemo devices, custom UI with DWM rules, device rules, scheduler heartbeat, and location-based sunrise/sunset scheduling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@wemo-manager/core",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.js"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @wemo-manager/core
|
||||
*
|
||||
* Shared utilities for both the Electron desktop app and the Homebridge plugin.
|
||||
*/
|
||||
|
||||
const sun = require('./sun');
|
||||
const types = require('./types');
|
||||
|
||||
module.exports = {
|
||||
...sun,
|
||||
...types,
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Calculate sunrise and sunset times for a given location and date.
|
||||
* Pure JS – no external dependencies.
|
||||
*
|
||||
* Algorithm: NOAA Solar Calculator (Jean Meeus, Astronomical Algorithms).
|
||||
*
|
||||
* @param {number} lat Latitude in decimal degrees (positive = North)
|
||||
* @param {number} lng Longitude in decimal degrees (positive = East)
|
||||
* @param {Date} date Date to calculate for (default: today)
|
||||
* @returns {{ sunrise: number|null, sunset: number|null }}
|
||||
* Times as integer seconds from LOCAL midnight.
|
||||
* null for each value if polar day or polar night.
|
||||
*/
|
||||
function sunTimes(lat, lng, date = new Date()) {
|
||||
const D2R = Math.PI / 180;
|
||||
const R2D = 180 / Math.PI;
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
|
||||
const A = Math.floor((14 - month) / 12);
|
||||
const Y = year + 4800 - A;
|
||||
const M = month + 12 * A - 3;
|
||||
const JDN = day + Math.floor((153 * M + 2) / 5) + 365 * Y
|
||||
+ Math.floor(Y / 4) - Math.floor(Y / 100) + Math.floor(Y / 400) - 32045;
|
||||
const JD = JDN - 0.5;
|
||||
|
||||
const T = (JD - 2451545.0) / 36525.0;
|
||||
|
||||
let L0 = 280.46646 + T * (36000.76983 + T * 0.0003032);
|
||||
L0 = ((L0 % 360) + 360) % 360;
|
||||
|
||||
let Mdeg = 357.52911 + T * (35999.05029 - 0.0001537 * T);
|
||||
Mdeg = ((Mdeg % 360) + 360) % 360;
|
||||
const Mrad = Mdeg * D2R;
|
||||
|
||||
const C = (1.914602 - T * (0.004817 + 0.000014 * T)) * Math.sin(Mrad)
|
||||
+ (0.019993 - 0.000101 * T) * Math.sin(2 * Mrad)
|
||||
+ 0.000289 * Math.sin(3 * Mrad);
|
||||
|
||||
const omega = 125.04 - 1934.136 * T;
|
||||
const lambda = (L0 + C) - 0.00569 - 0.00478 * Math.sin(omega * D2R);
|
||||
|
||||
const eps0 = 23.0
|
||||
+ (26.0 + (21.448 - T * (46.8150 + T * (0.00059 - T * 0.001813))) / 60.0) / 60.0;
|
||||
const eps = (eps0 + 0.00256 * Math.cos(omega * D2R)) * D2R;
|
||||
|
||||
const sinDec = Math.sin(eps) * Math.sin(lambda * D2R);
|
||||
const decl = Math.asin(sinDec);
|
||||
|
||||
const e = 0.016708634 - T * (0.000042037 + 0.0000001267 * T);
|
||||
const y = Math.pow(Math.tan(eps / 2), 2);
|
||||
const EqT = 4 * R2D * (
|
||||
y * Math.sin(2 * L0 * D2R)
|
||||
- 2 * e * Math.sin(Mrad)
|
||||
+ 4 * e * y * Math.sin(Mrad) * Math.cos(2 * L0 * D2R)
|
||||
- 0.5 * y * y * Math.sin(4 * L0 * D2R)
|
||||
- 1.25 * e * e * Math.sin(2 * Mrad)
|
||||
);
|
||||
|
||||
const cosHA = (Math.cos(90.833 * D2R) - Math.sin(lat * D2R) * sinDec)
|
||||
/ (Math.cos(lat * D2R) * Math.cos(decl));
|
||||
|
||||
if (cosHA < -1 || cosHA > 1) {
|
||||
return { sunrise: null, sunset: null };
|
||||
}
|
||||
|
||||
const HA = Math.acos(cosHA) * R2D;
|
||||
const tzOffsetMin = -date.getTimezoneOffset();
|
||||
const solarNoon = 720.0 - 4.0 * lng - EqT + tzOffsetMin;
|
||||
|
||||
return {
|
||||
sunrise: Math.round((solarNoon - HA * 4.0) * 60),
|
||||
sunset: Math.round((solarNoon + HA * 4.0) * 60),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { sunTimes };
|
||||
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
|
||||
/** Day numbers: 1=Monday ... 7=Sunday (Wemo convention) */
|
||||
const DAY_NUMBERS = { Monday:1, Tuesday:2, Wednesday:3, Thursday:4, Friday:5, Saturday:6, Sunday:7 };
|
||||
const DAY_NAMES = { 1:'Monday', 2:'Tuesday', 3:'Wednesday', 4:'Thursday', 5:'Friday', 6:'Saturday', 7:'Sunday' };
|
||||
const DAY_SHORT = { 1:'Mon', 2:'Tue', 3:'Wed', 4:'Thu', 5:'Fri', 6:'Sat', 7:'Sun' };
|
||||
|
||||
/** Rule types stored in RULES.Type */
|
||||
const RULE_TYPES = {
|
||||
SCHEDULE: 'Schedule',
|
||||
AWAY: 'Away',
|
||||
COUNTDOWN: 'Countdown',
|
||||
LONG_PRESS: 'Long Press',
|
||||
};
|
||||
|
||||
/** Start/End action values */
|
||||
const ACTIONS = { ON: 1.0, OFF: 0.0, TOGGLE: 2.0, NONE: -1.0 };
|
||||
|
||||
/** Network status codes returned by GetNetworkStatus */
|
||||
const NETWORK_STATUS = { FAILED: '0', SUCCESS: '1', WRONG_PASSWORD: '2', CONNECTING: '3' };
|
||||
|
||||
/** Wemo device reset codes for ReSetup action */
|
||||
const RESET_CODES = { CLEAR_DATA: 1, FACTORY_RESET: 2, CLEAR_WIFI: 5 };
|
||||
|
||||
/** Default RULEDEVICES field values */
|
||||
const RD_DEFAULTS = {
|
||||
GroupID: 0,
|
||||
RuleDuration: 0,
|
||||
StartAction: 1.0,
|
||||
EndAction: -1.0,
|
||||
SensorDuration: 2,
|
||||
Type: -1,
|
||||
Value: -1,
|
||||
Level: -1,
|
||||
ZBCapabilityStart: '',
|
||||
ZBCapabilityEnd: '',
|
||||
OnModeOffset: -1,
|
||||
OffModeOffset: -1,
|
||||
CountdownTime: 0,
|
||||
EndTime: -1,
|
||||
};
|
||||
|
||||
/** Sun time sentinel codes stored in RULEDEVICES.StartTime / EndTime */
|
||||
const SUN_CODES = { SUNRISE: -2, SUNSET: -3 };
|
||||
|
||||
function namesToDayNumbers(names) {
|
||||
return names.map((n) => DAY_NUMBERS[n]).filter(Boolean).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function dayNumbersToNames(numbers) {
|
||||
return numbers.map((n) => DAY_NAMES[n]).filter(Boolean);
|
||||
}
|
||||
|
||||
function dayNumbersToShort(numbers) {
|
||||
return numbers.map((n) => DAY_SHORT[n]).filter(Boolean);
|
||||
}
|
||||
|
||||
function timeToSecs(hhmm) {
|
||||
if (!hhmm || !hhmm.includes(':')) return 0;
|
||||
const [h, m] = hhmm.split(':').map(Number);
|
||||
return h * 3600 + m * 60;
|
||||
}
|
||||
|
||||
function secsToHHMM(secs) {
|
||||
if (secs === undefined || secs === null || secs < 0) return '00:00';
|
||||
const h = Math.floor(secs / 3600) % 24;
|
||||
const m = Math.floor((secs % 3600) / 60);
|
||||
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DAY_NUMBERS, DAY_NAMES, DAY_SHORT,
|
||||
RULE_TYPES, ACTIONS, NETWORK_STATUS, RESET_CODES, RD_DEFAULTS, SUN_CODES,
|
||||
namesToDayNumbers, dayNumbersToNames, dayNumbersToShort,
|
||||
timeToSecs, secsToHHMM,
|
||||
};
|
||||
Reference in New Issue
Block a user