ember-mug 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +13 -0
  5. package/dist/components/App.d.ts +2 -0
  6. package/dist/components/App.js +120 -0
  7. package/dist/components/BatteryDisplay.d.ts +9 -0
  8. package/dist/components/BatteryDisplay.js +25 -0
  9. package/dist/components/ColorControl.d.ts +9 -0
  10. package/dist/components/ColorControl.js +71 -0
  11. package/dist/components/ConnectionStatus.d.ts +10 -0
  12. package/dist/components/ConnectionStatus.js +9 -0
  13. package/dist/components/Header.d.ts +7 -0
  14. package/dist/components/Header.js +5 -0
  15. package/dist/components/HelpDisplay.d.ts +6 -0
  16. package/dist/components/HelpDisplay.js +5 -0
  17. package/dist/components/Presets.d.ts +11 -0
  18. package/dist/components/Presets.js +19 -0
  19. package/dist/components/SettingsView.d.ts +11 -0
  20. package/dist/components/SettingsView.js +34 -0
  21. package/dist/components/TemperatureControl.d.ts +10 -0
  22. package/dist/components/TemperatureControl.js +38 -0
  23. package/dist/components/TemperatureDisplay.d.ts +10 -0
  24. package/dist/components/TemperatureDisplay.js +40 -0
  25. package/dist/hooks/useMug.d.ts +14 -0
  26. package/dist/hooks/useMug.js +112 -0
  27. package/dist/index.d.ts +5 -0
  28. package/dist/index.js +5 -0
  29. package/dist/lib/bluetooth.d.ts +43 -0
  30. package/dist/lib/bluetooth.js +334 -0
  31. package/dist/lib/settings.d.ts +28 -0
  32. package/dist/lib/settings.js +73 -0
  33. package/dist/lib/types.d.ts +57 -0
  34. package/dist/lib/types.js +34 -0
  35. package/dist/lib/utils.d.ts +20 -0
  36. package/dist/lib/utils.js +152 -0
  37. package/package.json +64 -0
@@ -0,0 +1,34 @@
1
+ export var LiquidState;
2
+ (function (LiquidState) {
3
+ LiquidState[LiquidState["Empty"] = 1] = "Empty";
4
+ LiquidState[LiquidState["Filling"] = 2] = "Filling";
5
+ LiquidState[LiquidState["Cooling"] = 4] = "Cooling";
6
+ LiquidState[LiquidState["Heating"] = 5] = "Heating";
7
+ LiquidState[LiquidState["StableTemperature"] = 6] = "StableTemperature";
8
+ })(LiquidState || (LiquidState = {}));
9
+ export var TemperatureUnit;
10
+ (function (TemperatureUnit) {
11
+ TemperatureUnit[TemperatureUnit["Celsius"] = 0] = "Celsius";
12
+ TemperatureUnit[TemperatureUnit["Fahrenheit"] = 1] = "Fahrenheit";
13
+ })(TemperatureUnit || (TemperatureUnit = {}));
14
+ export const DEFAULT_PRESETS = [
15
+ { id: '1', name: 'Latte', icon: '☕', temperature: 52.0 },
16
+ { id: '2', name: 'Coffee', icon: '🍵', temperature: 55.0 },
17
+ { id: '3', name: 'Tea', icon: '🍃', temperature: 60.0 },
18
+ ];
19
+ export const EMBER_SERVICE_UUID = 'fc543622236c4c948fa9944a3e5353fa';
20
+ export const EMBER_CHARACTERISTICS = {
21
+ MUG_NAME: 'fc540001236c4c948fa9944a3e5353fa',
22
+ CURRENT_TEMP: 'fc540002236c4c948fa9944a3e5353fa',
23
+ TARGET_TEMP: 'fc540003236c4c948fa9944a3e5353fa',
24
+ TEMP_UNIT: 'fc540004236c4c948fa9944a3e5353fa',
25
+ BATTERY: 'fc540007236c4c948fa9944a3e5353fa',
26
+ LIQUID_STATE: 'fc540008236c4c948fa9944a3e5353fa',
27
+ PUSH_EVENTS: 'fc540012236c4c948fa9944a3e5353fa',
28
+ LED_COLOR: 'fc540014236c4c948fa9944a3e5353fa',
29
+ };
30
+ export const MIN_TEMP_CELSIUS = 50;
31
+ export const MAX_TEMP_CELSIUS = 63;
32
+ export const BATTERY_DRAIN_RATE_HEATING = 0.5; // % per minute when heating
33
+ export const BATTERY_DRAIN_RATE_MAINTAINING = 0.2; // % per minute when maintaining
34
+ export const BATTERY_CHARGE_RATE = 1.5; // % per minute when charging
@@ -0,0 +1,20 @@
1
+ import { TemperatureUnit, LiquidState } from './types.js';
2
+ export declare function formatTemperature(temp: number, unit: TemperatureUnit): string;
3
+ export declare function celsiusToFahrenheit(celsius: number): number;
4
+ export declare function fahrenheitToCelsius(fahrenheit: number): number;
5
+ export declare function formatBatteryLevel(level: number): string;
6
+ export declare function getBatteryIcon(level: number, isCharging: boolean): string;
7
+ export declare function getLiquidStateText(state: LiquidState): string;
8
+ export declare function getLiquidStateIcon(state: LiquidState): string;
9
+ export declare function clampTemperature(temp: number): number;
10
+ export declare function formatDuration(minutes: number): string;
11
+ export declare function estimateTimeToTargetTemp(currentTemp: number, targetTemp: number, liquidState: LiquidState): number | null;
12
+ export declare function estimateBatteryLife(batteryLevel: number, isCharging: boolean, liquidState: LiquidState): number | null;
13
+ export declare function getTemperatureColor(currentTemp: number, targetTemp: number): string;
14
+ export declare function interpolateColor(value: number, minColor: [number, number, number], maxColor: [number, number, number]): [number, number, number];
15
+ export declare function rgbToHex(r: number, g: number, b: number): string;
16
+ export declare function hexToRgb(hex: string): {
17
+ r: number;
18
+ g: number;
19
+ b: number;
20
+ } | null;
@@ -0,0 +1,152 @@
1
+ import { TemperatureUnit, LiquidState, MIN_TEMP_CELSIUS, MAX_TEMP_CELSIUS, BATTERY_DRAIN_RATE_HEATING, BATTERY_DRAIN_RATE_MAINTAINING, BATTERY_CHARGE_RATE, } from './types.js';
2
+ export function formatTemperature(temp, unit) {
3
+ if (unit === TemperatureUnit.Celsius) {
4
+ return `${temp.toFixed(1)}°C`;
5
+ }
6
+ const fahrenheit = (temp * 9) / 5 + 32;
7
+ return `${Math.round(fahrenheit)}°F`;
8
+ }
9
+ export function celsiusToFahrenheit(celsius) {
10
+ return (celsius * 9) / 5 + 32;
11
+ }
12
+ export function fahrenheitToCelsius(fahrenheit) {
13
+ return ((fahrenheit - 32) * 5) / 9;
14
+ }
15
+ export function formatBatteryLevel(level) {
16
+ return `${level}%`;
17
+ }
18
+ export function getBatteryIcon(level, isCharging) {
19
+ if (isCharging) {
20
+ return '⚡';
21
+ }
22
+ if (level >= 75)
23
+ return '████';
24
+ if (level >= 50)
25
+ return '███░';
26
+ if (level >= 25)
27
+ return '██░░';
28
+ if (level >= 10)
29
+ return '█░░░';
30
+ return '░░░░';
31
+ }
32
+ export function getLiquidStateText(state) {
33
+ switch (state) {
34
+ case LiquidState.Empty:
35
+ return 'Empty';
36
+ case LiquidState.Filling:
37
+ return 'Filling';
38
+ case LiquidState.Cooling:
39
+ return 'Cooling';
40
+ case LiquidState.Heating:
41
+ return 'Heating';
42
+ case LiquidState.StableTemperature:
43
+ return 'Perfect';
44
+ default:
45
+ return 'Unknown';
46
+ }
47
+ }
48
+ export function getLiquidStateIcon(state) {
49
+ switch (state) {
50
+ case LiquidState.Empty:
51
+ return '○';
52
+ case LiquidState.Filling:
53
+ return '◐';
54
+ case LiquidState.Cooling:
55
+ return '❄';
56
+ case LiquidState.Heating:
57
+ return '🔥';
58
+ case LiquidState.StableTemperature:
59
+ return '✓';
60
+ default:
61
+ return '?';
62
+ }
63
+ }
64
+ export function clampTemperature(temp) {
65
+ return Math.max(MIN_TEMP_CELSIUS, Math.min(MAX_TEMP_CELSIUS, temp));
66
+ }
67
+ export function formatDuration(minutes) {
68
+ if (minutes < 1) {
69
+ return '< 1 min';
70
+ }
71
+ if (minutes < 60) {
72
+ return `${Math.round(minutes)} min`;
73
+ }
74
+ const hours = Math.floor(minutes / 60);
75
+ const mins = Math.round(minutes % 60);
76
+ if (mins === 0) {
77
+ return `${hours}h`;
78
+ }
79
+ return `${hours}h ${mins}m`;
80
+ }
81
+ export function estimateTimeToTargetTemp(currentTemp, targetTemp, liquidState) {
82
+ if (liquidState === LiquidState.Empty) {
83
+ return null;
84
+ }
85
+ const tempDiff = Math.abs(targetTemp - currentTemp);
86
+ if (tempDiff < 0.5) {
87
+ return 0; // Already at target
88
+ }
89
+ // Estimate based on typical Ember mug heating/cooling rates
90
+ // Heating: approximately 1°C per minute
91
+ // Cooling: approximately 0.5°C per minute (slower due to insulation)
92
+ const isHeating = currentTemp < targetTemp;
93
+ const ratePerMinute = isHeating ? 1.0 : 0.5;
94
+ return tempDiff / ratePerMinute;
95
+ }
96
+ export function estimateBatteryLife(batteryLevel, isCharging, liquidState) {
97
+ if (isCharging) {
98
+ // Estimate time to full charge
99
+ const remaining = 100 - batteryLevel;
100
+ if (remaining <= 0)
101
+ return 0;
102
+ return remaining / BATTERY_CHARGE_RATE;
103
+ }
104
+ if (batteryLevel <= 0) {
105
+ return 0;
106
+ }
107
+ // Determine drain rate based on mug state
108
+ let drainRate;
109
+ switch (liquidState) {
110
+ case LiquidState.Heating:
111
+ drainRate = BATTERY_DRAIN_RATE_HEATING;
112
+ break;
113
+ case LiquidState.StableTemperature:
114
+ case LiquidState.Cooling:
115
+ drainRate = BATTERY_DRAIN_RATE_MAINTAINING;
116
+ break;
117
+ default:
118
+ drainRate = 0.1; // Minimal drain when empty or idle
119
+ }
120
+ return batteryLevel / drainRate;
121
+ }
122
+ export function getTemperatureColor(currentTemp, targetTemp) {
123
+ const diff = currentTemp - targetTemp;
124
+ if (Math.abs(diff) < 1) {
125
+ return 'green'; // At target
126
+ }
127
+ if (diff > 0) {
128
+ return 'red'; // Too hot
129
+ }
130
+ return 'blue'; // Too cold
131
+ }
132
+ export function interpolateColor(value, minColor, maxColor) {
133
+ const clampedValue = Math.max(0, Math.min(1, value));
134
+ return [
135
+ Math.round(minColor[0] + (maxColor[0] - minColor[0]) * clampedValue),
136
+ Math.round(minColor[1] + (maxColor[1] - minColor[1]) * clampedValue),
137
+ Math.round(minColor[2] + (maxColor[2] - minColor[2]) * clampedValue),
138
+ ];
139
+ }
140
+ export function rgbToHex(r, g, b) {
141
+ return `#${[r, g, b].map((c) => c.toString(16).padStart(2, '0')).join('')}`;
142
+ }
143
+ export function hexToRgb(hex) {
144
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
145
+ return result
146
+ ? {
147
+ r: parseInt(result[1], 16),
148
+ g: parseInt(result[2], 16),
149
+ b: parseInt(result[3], 16),
150
+ }
151
+ : null;
152
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "ember-mug",
3
+ "version": "0.1.3",
4
+ "description": "A CLI app for controlling Ember mugs via Bluetooth",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "ember-mug": "dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "clean": "rm -rf dist",
19
+ "prebuild": "npm run clean",
20
+ "start": "node dist/cli.js",
21
+ "dev": "tsx src/cli.tsx",
22
+ "prepublishOnly": "npm run build",
23
+ "version": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "ember",
27
+ "ember-mug",
28
+ "mug",
29
+ "cli",
30
+ "bluetooth",
31
+ "ble",
32
+ "temperature",
33
+ "smart-mug"
34
+ ],
35
+ "author": "",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/singerbj/ember-cli.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/singerbj/ember-cli/issues"
43
+ },
44
+ "homepage": "https://github.com/singerbj/ember-cli#readme",
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "dependencies": {
49
+ "@abandonware/noble": "^1.9.2-25",
50
+ "chalk": "^5.3.0",
51
+ "conf": "^12.0.0",
52
+ "ink": "^5.0.1",
53
+ "ink-select-input": "^6.0.0",
54
+ "ink-spinner": "^5.0.0",
55
+ "ink-text-input": "^6.0.0",
56
+ "react": "^18.3.1"
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^20.11.0",
60
+ "@types/react": "^18.3.3",
61
+ "tsx": "^4.7.0",
62
+ "typescript": "^5.3.3"
63
+ }
64
+ }