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:
SRS IT
2026-03-28 16:30:43 -04:00
commit 27be1892ed
75 changed files with 14322 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"name": "@wemo-manager/core",
"version": "2.0.0",
"private": true,
"main": "src/index.js"
}
+15
View File
@@ -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,
};
+81
View File
@@ -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 };
+76
View File
@@ -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,
};