@yunlefun-home/air-conditioner-core 0.3.0 → 0.4.0
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.
- package/dist/index.cjs +513 -46
- package/dist/index.d.cts +301 -19
- package/dist/index.d.ts +301 -19
- package/dist/index.js +486 -47
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -32,7 +32,13 @@ const defaultAcState = {
|
|
|
32
32
|
mode: "cold",
|
|
33
33
|
status: false,
|
|
34
34
|
temperature: 26,
|
|
35
|
-
|
|
35
|
+
temperatures: {
|
|
36
|
+
cold: 26,
|
|
37
|
+
hot: 26
|
|
38
|
+
},
|
|
39
|
+
sound: "normal",
|
|
40
|
+
swing: false,
|
|
41
|
+
fan: "auto"
|
|
36
42
|
};
|
|
37
43
|
/**
|
|
38
44
|
* 遥控器「声音」键依次切换的顺序。
|
|
@@ -42,6 +48,13 @@ const acSoundModes = [
|
|
|
42
48
|
"sleep",
|
|
43
49
|
"mute"
|
|
44
50
|
];
|
|
51
|
+
/**
|
|
52
|
+
* 各模式的中文名称(遥控器小屏与按键提示共用)。
|
|
53
|
+
*/
|
|
54
|
+
const acModeLabels = {
|
|
55
|
+
cold: "制冷",
|
|
56
|
+
hot: "制热"
|
|
57
|
+
};
|
|
45
58
|
const acSoundModeLabels = {
|
|
46
59
|
normal: "标准",
|
|
47
60
|
sleep: "睡眠",
|
|
@@ -55,6 +68,48 @@ const acSoundVolumes = {
|
|
|
55
68
|
sleep: .3,
|
|
56
69
|
mute: 0
|
|
57
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* 遥控器「风速」键依次切换的顺序。
|
|
73
|
+
*/
|
|
74
|
+
const acFanSpeeds = [
|
|
75
|
+
"auto",
|
|
76
|
+
"low",
|
|
77
|
+
"medium",
|
|
78
|
+
"high"
|
|
79
|
+
];
|
|
80
|
+
const acFanSpeedLabels = {
|
|
81
|
+
auto: "自动",
|
|
82
|
+
low: "低风",
|
|
83
|
+
medium: "中风",
|
|
84
|
+
high: "高风"
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* 小屏上点亮的风速格数(1–3)。自动风速由空调自己决定,显示为 0 并另加「自动」标记。
|
|
88
|
+
*/
|
|
89
|
+
const acFanBars = {
|
|
90
|
+
auto: 0,
|
|
91
|
+
low: 1,
|
|
92
|
+
medium: 2,
|
|
93
|
+
high: 3
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* 固定风速下风声的相对音量(0–1);自动风速沿用开机后的「达标 / 发力」循环。
|
|
97
|
+
*/
|
|
98
|
+
const acFanSoundLevels = {
|
|
99
|
+
low: .45,
|
|
100
|
+
medium: .7,
|
|
101
|
+
high: 1
|
|
102
|
+
};
|
|
103
|
+
function isAcFanSpeed(value) {
|
|
104
|
+
return acFanSpeeds.includes(value);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 风速键的下一档:自动 → 低 → 中 → 高 → 自动。
|
|
108
|
+
*/
|
|
109
|
+
function nextAcFanSpeed(fan) {
|
|
110
|
+
const index = Math.max(acFanSpeeds.indexOf(fan), 0);
|
|
111
|
+
return acFanSpeeds[(index + 1) % acFanSpeeds.length];
|
|
112
|
+
}
|
|
58
113
|
function isAcSoundMode(value) {
|
|
59
114
|
return acSoundModes.includes(value);
|
|
60
115
|
}
|
|
@@ -126,6 +181,25 @@ function createDefaultAudioContext() {
|
|
|
126
181
|
return AudioContextConstructor ? new AudioContextConstructor() : null;
|
|
127
182
|
}
|
|
128
183
|
/**
|
|
184
|
+
* 合成一声很轻的「嗒」:音高快速下滑的正弦波,短促起音、指数衰减,像水珠落在窗台上。
|
|
185
|
+
*/
|
|
186
|
+
function playDrip(context, output) {
|
|
187
|
+
const start = context.currentTime;
|
|
188
|
+
const oscillator = context.createOscillator();
|
|
189
|
+
const gain = context.createGain();
|
|
190
|
+
oscillator.type = "sine";
|
|
191
|
+
oscillator.frequency.setValueAtTime(1500, start);
|
|
192
|
+
oscillator.frequency.exponentialRampToValueAtTime(520, start + .07);
|
|
193
|
+
gain.gain.setValueAtTime(1e-4, start);
|
|
194
|
+
gain.gain.exponentialRampToValueAtTime(.12, start + .004);
|
|
195
|
+
gain.gain.exponentialRampToValueAtTime(1e-4, start + .16);
|
|
196
|
+
oscillator.connect(gain);
|
|
197
|
+
gain.connect(output);
|
|
198
|
+
oscillator.start(start);
|
|
199
|
+
oscillator.stop(start + .18);
|
|
200
|
+
oscillator.onended = () => gain.disconnect();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
129
203
|
* 等功率淡入 / 淡出曲线,噪声类声音交叉淡化时中段不会变小。
|
|
130
204
|
*/
|
|
131
205
|
function fadeCurve(direction) {
|
|
@@ -164,6 +238,11 @@ function stopElement(element) {
|
|
|
164
238
|
function createAcAudioController(elements, options = {}) {
|
|
165
239
|
const random = options.random ?? Math.random;
|
|
166
240
|
let soundMode = options.soundMode ?? "normal";
|
|
241
|
+
let fanSpeed = options.fanSpeed ?? "auto";
|
|
242
|
+
/** 开机后风声循环已经接上(启动声结束) */
|
|
243
|
+
let fanRunning = false;
|
|
244
|
+
/** 每次重启「达标 / 发力」循环自增,丢弃旧循环的回调 */
|
|
245
|
+
let fanCycle = 0;
|
|
167
246
|
/** undefined:尚未尝试;null:不可用 */
|
|
168
247
|
let engine;
|
|
169
248
|
let run;
|
|
@@ -298,20 +377,32 @@ function createAcAudioController(elements, options = {}) {
|
|
|
298
377
|
gain.linearRampToValueAtTime(level, now + rampMs / 1e3);
|
|
299
378
|
} else tweenFallbackFan(level, rampMs);
|
|
300
379
|
}
|
|
380
|
+
/** 风声循环刚接上时的音量:自动风速从满音量开始,固定风速保持各自的音量。 */
|
|
381
|
+
function initialFanLevel() {
|
|
382
|
+
return fanSpeed === "auto" ? 1 : acFanSoundLevels[fanSpeed];
|
|
383
|
+
}
|
|
301
384
|
/**
|
|
302
|
-
*
|
|
385
|
+
* 自动风速:室温达标后风声变小;之后不定时随压缩机发力变大,再回落。
|
|
386
|
+
* 固定风速不走这个循环。
|
|
303
387
|
*/
|
|
304
388
|
function startFanCycle() {
|
|
389
|
+
fanRunning = true;
|
|
390
|
+
const cycle = ++fanCycle;
|
|
391
|
+
if (fanSpeed !== "auto") return;
|
|
392
|
+
const current = () => cycle === fanCycle && fanSpeed === "auto";
|
|
305
393
|
const quiet = () => {
|
|
306
394
|
later(() => {
|
|
395
|
+
if (!current()) return;
|
|
307
396
|
setFanLevel(soundTimings.surgeLevel, soundTimings.surgeRampMs);
|
|
308
397
|
later(() => {
|
|
398
|
+
if (!current()) return;
|
|
309
399
|
setFanLevel(soundTimings.settleLevel, soundTimings.surgeRampMs);
|
|
310
400
|
quiet();
|
|
311
401
|
}, randomBetween(soundTimings.surgeDurationMs, random));
|
|
312
402
|
}, randomBetween(soundTimings.quietDurationMs, random));
|
|
313
403
|
};
|
|
314
404
|
later(() => {
|
|
405
|
+
if (!current()) return;
|
|
315
406
|
setFanLevel(soundTimings.settleLevel, soundTimings.settleRampMs);
|
|
316
407
|
quiet();
|
|
317
408
|
}, soundTimings.settleAfterMs);
|
|
@@ -320,6 +411,7 @@ function createAcAudioController(elements, options = {}) {
|
|
|
320
411
|
const output = web.context.createGain();
|
|
321
412
|
output.connect(web.master);
|
|
322
413
|
const fan = web.context.createGain();
|
|
414
|
+
fan.gain.value = initialFanLevel();
|
|
323
415
|
fan.connect(output);
|
|
324
416
|
const target = {
|
|
325
417
|
output,
|
|
@@ -360,7 +452,7 @@ function createAcAudioController(elements, options = {}) {
|
|
|
360
452
|
applyElementVolume(work, 0);
|
|
361
453
|
if (work.loop !== void 0) work.loop = true;
|
|
362
454
|
playElement(work);
|
|
363
|
-
tweenFallbackFan(
|
|
455
|
+
tweenFallbackFan(initialFanLevel(), soundTimings.loopCrossfadeMs);
|
|
364
456
|
startFanCycle();
|
|
365
457
|
}, soundTimings.startDelayMs);
|
|
366
458
|
}
|
|
@@ -371,6 +463,8 @@ function createAcAudioController(elements, options = {}) {
|
|
|
371
463
|
}
|
|
372
464
|
function powerOn() {
|
|
373
465
|
const id = ++session;
|
|
466
|
+
fanRunning = false;
|
|
467
|
+
fanCycle++;
|
|
374
468
|
clearTimers();
|
|
375
469
|
cancelFallbackTween();
|
|
376
470
|
stopWeb(0);
|
|
@@ -389,6 +483,8 @@ function createAcAudioController(elements, options = {}) {
|
|
|
389
483
|
}
|
|
390
484
|
function powerOff() {
|
|
391
485
|
session++;
|
|
486
|
+
fanRunning = false;
|
|
487
|
+
fanCycle++;
|
|
392
488
|
clearTimers();
|
|
393
489
|
stopWeb(soundTimings.stopFadeMs);
|
|
394
490
|
stopFallback(soundTimings.stopFadeMs);
|
|
@@ -407,6 +503,13 @@ function createAcAudioController(elements, options = {}) {
|
|
|
407
503
|
applyElementVolume(elements.beep);
|
|
408
504
|
playElement(elements.beep);
|
|
409
505
|
},
|
|
506
|
+
drip() {
|
|
507
|
+
if (soundMode !== "normal") return;
|
|
508
|
+
const web = ensureEngine();
|
|
509
|
+
if (!web || typeof web.context.createOscillator !== "function") return;
|
|
510
|
+
resume(web);
|
|
511
|
+
playDrip(web.context, web.master);
|
|
512
|
+
},
|
|
410
513
|
toggleStatusAudio(previousStatus) {
|
|
411
514
|
if (previousStatus) powerOff();
|
|
412
515
|
else powerOn();
|
|
@@ -418,6 +521,21 @@ function createAcAudioController(elements, options = {}) {
|
|
|
418
521
|
applyElementVolume(elements.start);
|
|
419
522
|
applyElementVolume(elements.work, fallbackFanLevel);
|
|
420
523
|
},
|
|
524
|
+
setFanSpeed(fan) {
|
|
525
|
+
if (fan === fanSpeed) return;
|
|
526
|
+
fanSpeed = fan;
|
|
527
|
+
if (!fanRunning) {
|
|
528
|
+
if (run) run.fan.gain.value = initialFanLevel();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (fan === "auto") {
|
|
532
|
+
setFanLevel(1, soundTimings.loopCrossfadeMs);
|
|
533
|
+
startFanCycle();
|
|
534
|
+
} else {
|
|
535
|
+
fanCycle++;
|
|
536
|
+
setFanLevel(acFanSoundLevels[fan], soundTimings.loopCrossfadeMs);
|
|
537
|
+
}
|
|
538
|
+
},
|
|
421
539
|
dispose() {
|
|
422
540
|
session++;
|
|
423
541
|
clearTimers();
|
|
@@ -430,6 +548,54 @@ function createAcAudioController(elements, options = {}) {
|
|
|
430
548
|
};
|
|
431
549
|
}
|
|
432
550
|
//#endregion
|
|
551
|
+
//#region src/condensation.ts
|
|
552
|
+
/** 风速越大,雾气越浓、冷凝水越快 */
|
|
553
|
+
const fanFactor = {
|
|
554
|
+
auto: .8,
|
|
555
|
+
low: .55,
|
|
556
|
+
medium: .8,
|
|
557
|
+
high: 1
|
|
558
|
+
};
|
|
559
|
+
/** 高于这个设定温度不滴水:冷凝水不该常见,只在开得很冷时偶尔出现 */
|
|
560
|
+
const acDripMaxTemperature = 22;
|
|
561
|
+
/** 最冷(16 °C)与 {@link acDripMaxTemperature} 时,两滴水之间的间隔(ms) */
|
|
562
|
+
const acDripIntervalRange = [9e3, 18e3];
|
|
563
|
+
/**
|
|
564
|
+
* 出风口雾气的浓度(#70),0–1:只在制冷开机时出现,设定温度越低、风越大越浓。
|
|
565
|
+
*
|
|
566
|
+
* 视图把它写进 CSS 变量 `--acw-mist`,3D 等其他渲染器也可以按它控制粒子数量。
|
|
567
|
+
*/
|
|
568
|
+
function getAcMist(state) {
|
|
569
|
+
if (!state.status || state.mode !== "cold") return 0;
|
|
570
|
+
const cold = .2 + .8 * (31 - state.temperature) / 15;
|
|
571
|
+
const mist = Math.min(1, Math.max(.2, cold)) * fanFactor[state.fan ?? "auto"];
|
|
572
|
+
return Math.round(mist * 100) / 100;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* 冷凝水滴落的间隔(#21,ms):只在制冷开机、设定温度不高于 {@link acDripMaxTemperature} 时滴水,
|
|
576
|
+
* 越冷越频繁,高风再快一些;不滴水时返回 `undefined`。
|
|
577
|
+
*
|
|
578
|
+
* 视图把它写进 CSS 变量 `--acw-drip-cycle`,水珠在出风口边缘慢慢聚起再落下,开机后要等一整个间隔才滴第一滴。
|
|
579
|
+
*/
|
|
580
|
+
function getAcDripInterval(state) {
|
|
581
|
+
if (!state.status || state.mode !== "cold" || state.temperature > 22) return void 0;
|
|
582
|
+
const [fastest, slowest] = acDripIntervalRange;
|
|
583
|
+
const progress = (state.temperature - 16) / 6;
|
|
584
|
+
const interval = fastest + (slowest - fastest) * Math.min(1, Math.max(0, progress));
|
|
585
|
+
return Math.round(interval * (state.fan === "high" ? .8 : 1));
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* 视图要写到空调根元素上的雾气 / 滴水样式变量与属性。
|
|
589
|
+
*/
|
|
590
|
+
function getAcCondensationStyle(state) {
|
|
591
|
+
const interval = getAcDripInterval(state);
|
|
592
|
+
return {
|
|
593
|
+
mist: getAcMist(state),
|
|
594
|
+
dripCycle: interval === void 0 ? void 0 : `${interval}ms`,
|
|
595
|
+
drip: interval === void 0 ? "off" : "on"
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
//#endregion
|
|
433
599
|
//#region src/motion.ts
|
|
434
600
|
/**
|
|
435
601
|
* 动效 token:各框架视图层共用的时长与缓动曲线。
|
|
@@ -448,7 +614,9 @@ const acMotion = {
|
|
|
448
614
|
/** 导风板、出风淡入淡出 */
|
|
449
615
|
slow: 420,
|
|
450
616
|
/** 出风气流单次循环 */
|
|
451
|
-
flow: 1800
|
|
617
|
+
flow: 1800,
|
|
618
|
+
/** 扫风:左右摆动一个来回 */
|
|
619
|
+
swing: 5200
|
|
452
620
|
},
|
|
453
621
|
easing: {
|
|
454
622
|
/** 常规状态切换 */
|
|
@@ -470,7 +638,8 @@ const acReducedMotionDuration = {
|
|
|
470
638
|
fast: 80,
|
|
471
639
|
base: 120,
|
|
472
640
|
slow: 120,
|
|
473
|
-
flow: 0
|
|
641
|
+
flow: 0,
|
|
642
|
+
swing: 0
|
|
474
643
|
};
|
|
475
644
|
const acReducedMotionQuery = "(prefers-reduced-motion: reduce)";
|
|
476
645
|
/**
|
|
@@ -592,6 +761,124 @@ function observeAcTilt(target, onTilt, maxTilt = 4) {
|
|
|
592
761
|
};
|
|
593
762
|
}
|
|
594
763
|
//#endregion
|
|
764
|
+
//#region src/model.ts
|
|
765
|
+
const acForms = ["wall", "tower"];
|
|
766
|
+
const acFormLabels = {
|
|
767
|
+
wall: "挂机",
|
|
768
|
+
tower: "柜机"
|
|
769
|
+
};
|
|
770
|
+
/** 网页用的模型文件名(站点的 `public/models/` 下) */
|
|
771
|
+
const acModelFiles = {
|
|
772
|
+
wall: "CuteAC.glb",
|
|
773
|
+
tower: "CuteACTower.glb"
|
|
774
|
+
};
|
|
775
|
+
/** 渲染器要控制的命名节点,两种形态相同 */
|
|
776
|
+
const acModelParts = {
|
|
777
|
+
/** 导风板:挂机是一整块,柜机是一组百叶(子节点) */
|
|
778
|
+
louver: "Louver",
|
|
779
|
+
/** 0.25 × 0.1、UV 为 0–1 的平面,贴温度画面 */
|
|
780
|
+
display: "Display",
|
|
781
|
+
/** 指示灯 */
|
|
782
|
+
led: "Led"
|
|
783
|
+
};
|
|
784
|
+
const acLouverMotion = {
|
|
785
|
+
wall: {
|
|
786
|
+
pivot: "node",
|
|
787
|
+
openAngle: 40
|
|
788
|
+
},
|
|
789
|
+
tower: {
|
|
790
|
+
pivot: "children",
|
|
791
|
+
openAngle: 30
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
const acAirflowAnchors = {
|
|
795
|
+
wall: {
|
|
796
|
+
offset: [
|
|
797
|
+
0,
|
|
798
|
+
-.1,
|
|
799
|
+
.05
|
|
800
|
+
],
|
|
801
|
+
extent: 1.5,
|
|
802
|
+
direction: "down"
|
|
803
|
+
},
|
|
804
|
+
tower: {
|
|
805
|
+
offset: [
|
|
806
|
+
.24,
|
|
807
|
+
0,
|
|
808
|
+
.08
|
|
809
|
+
],
|
|
810
|
+
extent: .5,
|
|
811
|
+
direction: "side"
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
/** 指示灯与显示窗的颜色:开机时是模式色,关机是灰色(与 `ACPalette` 一致) */
|
|
815
|
+
const acModelColors = {
|
|
816
|
+
cold: "#5aa9ff",
|
|
817
|
+
hot: "#ff9f5a",
|
|
818
|
+
off: "#8e97aa"
|
|
819
|
+
};
|
|
820
|
+
function getAcLedColor(state) {
|
|
821
|
+
return state.status ? acModelColors[state.mode] : acModelColors.off;
|
|
822
|
+
}
|
|
823
|
+
function isAcForm(value) {
|
|
824
|
+
return value === "wall" || value === "tower";
|
|
825
|
+
}
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region src/press.ts
|
|
828
|
+
/**
|
|
829
|
+
* 让按键支持长按连续触发,和真的遥控器一样:按下立即触发一次,按住一会儿后连续触发并逐渐加速,
|
|
830
|
+
* 松开、移出或取消时停止。键盘的 Enter / 空格仍走 `click`,每按一次触发一次。
|
|
831
|
+
*
|
|
832
|
+
* @param element 按键
|
|
833
|
+
* @param fire 每次触发时调用
|
|
834
|
+
* @param options 节奏与停止条件
|
|
835
|
+
* @returns 解除绑定的清理函数
|
|
836
|
+
*/
|
|
837
|
+
function bindAcPressRepeat(element, fire, options = {}) {
|
|
838
|
+
const { delay = 500, interval = 200, minInterval = 100, value } = options;
|
|
839
|
+
let timer;
|
|
840
|
+
let wait = interval;
|
|
841
|
+
let last;
|
|
842
|
+
const stop = () => {
|
|
843
|
+
clearTimeout(timer);
|
|
844
|
+
timer = void 0;
|
|
845
|
+
};
|
|
846
|
+
const tick = () => {
|
|
847
|
+
if (value && value() === last) return stop();
|
|
848
|
+
last = value?.();
|
|
849
|
+
fire();
|
|
850
|
+
wait = Math.max(minInterval, wait * .85);
|
|
851
|
+
timer = setTimeout(tick, wait);
|
|
852
|
+
};
|
|
853
|
+
const onPointerDown = (event) => {
|
|
854
|
+
if (event.button !== 0) return;
|
|
855
|
+
stop();
|
|
856
|
+
last = value?.();
|
|
857
|
+
fire();
|
|
858
|
+
wait = interval;
|
|
859
|
+
timer = setTimeout(tick, delay);
|
|
860
|
+
};
|
|
861
|
+
const onClick = (event) => {
|
|
862
|
+
if (event.detail === 0) fire();
|
|
863
|
+
};
|
|
864
|
+
const onContextMenu = (event) => event.preventDefault();
|
|
865
|
+
element.addEventListener("pointerdown", onPointerDown);
|
|
866
|
+
element.addEventListener("pointerup", stop);
|
|
867
|
+
element.addEventListener("pointerleave", stop);
|
|
868
|
+
element.addEventListener("pointercancel", stop);
|
|
869
|
+
element.addEventListener("click", onClick);
|
|
870
|
+
element.addEventListener("contextmenu", onContextMenu);
|
|
871
|
+
return () => {
|
|
872
|
+
stop();
|
|
873
|
+
element.removeEventListener("pointerdown", onPointerDown);
|
|
874
|
+
element.removeEventListener("pointerup", stop);
|
|
875
|
+
element.removeEventListener("pointerleave", stop);
|
|
876
|
+
element.removeEventListener("pointercancel", stop);
|
|
877
|
+
element.removeEventListener("click", onClick);
|
|
878
|
+
element.removeEventListener("contextmenu", onContextMenu);
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
//#endregion
|
|
595
882
|
//#region src/reducer.ts
|
|
596
883
|
/**
|
|
597
884
|
* Merge a partial state over the defaults and clamp its temperature.
|
|
@@ -600,11 +887,38 @@ function observeAcTilt(target, onTilt, maxTilt = 4) {
|
|
|
600
887
|
* @param limits - Device limits; omit to keep the default 16–31 °C range
|
|
601
888
|
*/
|
|
602
889
|
function normalizeAcState(state, limits) {
|
|
890
|
+
const mode = state?.mode === "hot" ? "hot" : "cold";
|
|
891
|
+
const temperature = clampTemperature(state?.temperature ?? defaultAcState.temperature, limits);
|
|
892
|
+
const remembered = (other) => {
|
|
893
|
+
const value = state?.temperatures?.[other];
|
|
894
|
+
return typeof value === "number" && Number.isFinite(value) ? clampTemperature(value, limits) : temperature;
|
|
895
|
+
};
|
|
603
896
|
return {
|
|
604
897
|
...defaultAcState,
|
|
605
898
|
...state,
|
|
606
|
-
|
|
607
|
-
|
|
899
|
+
mode,
|
|
900
|
+
temperature,
|
|
901
|
+
temperatures: {
|
|
902
|
+
cold: mode === "cold" ? temperature : remembered("cold"),
|
|
903
|
+
hot: mode === "hot" ? temperature : remembered("hot")
|
|
904
|
+
},
|
|
905
|
+
sound: isAcSoundMode(state?.sound) ? state.sound : defaultAcState.sound,
|
|
906
|
+
swing: typeof state?.swing === "boolean" ? state.swing : defaultAcState.swing,
|
|
907
|
+
fan: isAcFanSpeed(state?.fan) ? state.fan : "auto"
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
/** Set the current mode's temperature, keeping `temperatures` in step. */
|
|
911
|
+
function withTemperature(state, temperature) {
|
|
912
|
+
const temperatures = {
|
|
913
|
+
cold: temperature,
|
|
914
|
+
hot: temperature,
|
|
915
|
+
...state.temperatures
|
|
916
|
+
};
|
|
917
|
+
temperatures[state.mode] = temperature;
|
|
918
|
+
return {
|
|
919
|
+
...state,
|
|
920
|
+
temperature,
|
|
921
|
+
temperatures
|
|
608
922
|
};
|
|
609
923
|
}
|
|
610
924
|
/**
|
|
@@ -630,14 +944,8 @@ function clampTemperature(temperature, limits) {
|
|
|
630
944
|
function acReducer(state, action, limits) {
|
|
631
945
|
const step = limits?.step ?? defaultAcLimits.step;
|
|
632
946
|
switch (action.type) {
|
|
633
|
-
case "increment": return
|
|
634
|
-
|
|
635
|
-
temperature: clampTemperature(state.temperature + step, limits)
|
|
636
|
-
};
|
|
637
|
-
case "decrement": return {
|
|
638
|
-
...state,
|
|
639
|
-
temperature: clampTemperature(state.temperature - step, limits)
|
|
640
|
-
};
|
|
947
|
+
case "increment": return withTemperature(state, clampTemperature(state.temperature + step, limits));
|
|
948
|
+
case "decrement": return withTemperature(state, clampTemperature(state.temperature - step, limits));
|
|
641
949
|
case "toggleStatus": return {
|
|
642
950
|
...state,
|
|
643
951
|
status: !state.status
|
|
@@ -646,10 +954,26 @@ function acReducer(state, action, limits) {
|
|
|
646
954
|
...state,
|
|
647
955
|
status: action.status
|
|
648
956
|
};
|
|
649
|
-
case "mode":
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
957
|
+
case "mode": {
|
|
958
|
+
const status = state.status || action.powerOn === true;
|
|
959
|
+
if (action.mode === state.mode) return status === state.status ? state : {
|
|
960
|
+
...state,
|
|
961
|
+
status
|
|
962
|
+
};
|
|
963
|
+
const temperatures = {
|
|
964
|
+
cold: state.temperature,
|
|
965
|
+
hot: state.temperature,
|
|
966
|
+
...state.temperatures,
|
|
967
|
+
[state.mode]: state.temperature
|
|
968
|
+
};
|
|
969
|
+
return {
|
|
970
|
+
...state,
|
|
971
|
+
status,
|
|
972
|
+
mode: action.mode,
|
|
973
|
+
temperature: clampTemperature(temperatures[action.mode] ?? state.temperature, limits),
|
|
974
|
+
temperatures
|
|
975
|
+
};
|
|
976
|
+
}
|
|
653
977
|
case "sound": return {
|
|
654
978
|
...state,
|
|
655
979
|
sound: isAcSoundMode(action.sound) ? action.sound : state.sound
|
|
@@ -658,6 +982,22 @@ function acReducer(state, action, limits) {
|
|
|
658
982
|
...state,
|
|
659
983
|
sound: nextAcSoundMode(state.sound)
|
|
660
984
|
};
|
|
985
|
+
case "toggleSwing": return {
|
|
986
|
+
...state,
|
|
987
|
+
swing: !state.swing
|
|
988
|
+
};
|
|
989
|
+
case "swing": return {
|
|
990
|
+
...state,
|
|
991
|
+
swing: action.swing === true
|
|
992
|
+
};
|
|
993
|
+
case "fan": return {
|
|
994
|
+
...state,
|
|
995
|
+
fan: isAcFanSpeed(action.fan) ? action.fan : state.fan
|
|
996
|
+
};
|
|
997
|
+
case "cycleFan": return {
|
|
998
|
+
...state,
|
|
999
|
+
fan: nextAcFanSpeed(state.fan)
|
|
1000
|
+
};
|
|
661
1001
|
case "update": return normalizeAcState({
|
|
662
1002
|
...state,
|
|
663
1003
|
...action.payload
|
|
@@ -665,6 +1005,32 @@ function acReducer(state, action, limits) {
|
|
|
665
1005
|
default: throw new Error("Unexpected Ac Action");
|
|
666
1006
|
}
|
|
667
1007
|
}
|
|
1008
|
+
/**
|
|
1009
|
+
* 节能建议的短文案(显示在小纸条上)与补充说明。
|
|
1010
|
+
*/
|
|
1011
|
+
function getEnergyTip(mode, temperature) {
|
|
1012
|
+
return mode === "cold" ? {
|
|
1013
|
+
message: `制冷调到 ${temperature}°C 以上更省电`,
|
|
1014
|
+
detail: "少开一度,为节能减排出一份力"
|
|
1015
|
+
} : {
|
|
1016
|
+
message: `制热调到 ${temperature}°C 以下更省电`,
|
|
1017
|
+
detail: "少开一度,为节能减排出一份力"
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* 创建一个提示「呈现器」:每个视图实例一个,记住提示过的节能建议,避免重复打扰。
|
|
1022
|
+
*/
|
|
1023
|
+
function createAcNotificationPresenter() {
|
|
1024
|
+
let lastTip;
|
|
1025
|
+
return (notification, state) => {
|
|
1026
|
+
if (notification.kind === "limit") return "shake";
|
|
1027
|
+
if (notification.kind === "setting") return "announce";
|
|
1028
|
+
const key = `${state.mode}:${state.temperature}`;
|
|
1029
|
+
if (key === lastTip) return "skip";
|
|
1030
|
+
lastTip = key;
|
|
1031
|
+
return "note";
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
668
1034
|
function getRecommendedTemperatureMessage(mode, temperature) {
|
|
669
1035
|
return `建议将空调的制${mode === "cold" ? "冷" : "热"}温度调至 ${temperature} 度以${mode === "cold" ? "上" : "下"},为节能减排贡献一份力量!`;
|
|
670
1036
|
}
|
|
@@ -675,6 +1041,12 @@ function getSoundModeMessage(sound) {
|
|
|
675
1041
|
mute: "空调不再发出任何声音"
|
|
676
1042
|
}[sound]}`;
|
|
677
1043
|
}
|
|
1044
|
+
function getFanSpeedMessage(fan) {
|
|
1045
|
+
return fan === "auto" ? "风速:自动,由空调根据室温调节" : `风速:${acFanSpeedLabels[fan]}`;
|
|
1046
|
+
}
|
|
1047
|
+
function getSwingMessage(swing) {
|
|
1048
|
+
return swing ? "扫风已开启:导风板上下摆动,出风左右扫" : "扫风已关闭:导风板停在当前位置";
|
|
1049
|
+
}
|
|
678
1050
|
/**
|
|
679
1051
|
* Visual tone for a state: `idle` when off, otherwise `cool` or `heat`.
|
|
680
1052
|
*
|
|
@@ -687,23 +1059,38 @@ function acTone(state) {
|
|
|
687
1059
|
function getAcActionNotification(previousState, nextState, action, limits = defaultAcLimits) {
|
|
688
1060
|
if (action.type === "increment" && previousState.temperature >= limits.max) return {
|
|
689
1061
|
message: "已经是最大温度啦!",
|
|
690
|
-
severity: "error"
|
|
1062
|
+
severity: "error",
|
|
1063
|
+
kind: "limit"
|
|
691
1064
|
};
|
|
692
1065
|
if (action.type === "decrement" && previousState.temperature <= limits.min) return {
|
|
693
1066
|
message: "已经是最小温度啦!",
|
|
694
|
-
severity: "error"
|
|
1067
|
+
severity: "error",
|
|
1068
|
+
kind: "limit"
|
|
695
1069
|
};
|
|
696
1070
|
if ((action.type === "sound" || action.type === "cycleSound") && nextState.sound !== previousState.sound) return {
|
|
697
1071
|
message: getSoundModeMessage(nextState.sound),
|
|
698
|
-
severity: "info"
|
|
1072
|
+
severity: "info",
|
|
1073
|
+
kind: "setting"
|
|
699
1074
|
};
|
|
700
|
-
if (action.type === "
|
|
701
|
-
message:
|
|
702
|
-
severity: "
|
|
1075
|
+
if ((action.type === "fan" || action.type === "cycleFan") && nextState.fan && nextState.fan !== previousState.fan) return {
|
|
1076
|
+
message: getFanSpeedMessage(nextState.fan),
|
|
1077
|
+
severity: "info",
|
|
1078
|
+
kind: "setting"
|
|
703
1079
|
};
|
|
704
|
-
if (action.type === "
|
|
705
|
-
message:
|
|
706
|
-
severity: "
|
|
1080
|
+
if ((action.type === "swing" || action.type === "toggleSwing") && nextState.swing !== previousState.swing) return {
|
|
1081
|
+
message: getSwingMessage(nextState.swing),
|
|
1082
|
+
severity: "info",
|
|
1083
|
+
kind: "setting"
|
|
1084
|
+
};
|
|
1085
|
+
if (action.type === "mode" && nextState.mode === "cold" && nextState.temperature < 26) return {
|
|
1086
|
+
...getEnergyTip("cold", 26),
|
|
1087
|
+
severity: "success",
|
|
1088
|
+
kind: "tip"
|
|
1089
|
+
};
|
|
1090
|
+
if (action.type === "mode" && nextState.mode === "hot" && nextState.temperature > 20) return {
|
|
1091
|
+
...getEnergyTip("hot", 20),
|
|
1092
|
+
severity: "success",
|
|
1093
|
+
kind: "tip"
|
|
707
1094
|
};
|
|
708
1095
|
}
|
|
709
1096
|
//#endregion
|
|
@@ -928,24 +1315,41 @@ function createComputePressureSource(options = {}) {
|
|
|
928
1315
|
};
|
|
929
1316
|
}
|
|
930
1317
|
//#endregion
|
|
931
|
-
//#region src/
|
|
1318
|
+
//#region src/icons.ts
|
|
932
1319
|
/**
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*/
|
|
936
|
-
/**
|
|
937
|
-
* 遥控器按钮图标(24 × 24 viewBox 的 SVG path)。
|
|
1320
|
+
* 遥控器按钮与提示图标(24 × 24 viewBox 的 SVG path),全部来自 Material Design
|
|
1321
|
+
* Icons,和站点通过 UnoCSS presetIcons 使用的 i-mdi-* 是同一套。
|
|
938
1322
|
*/
|
|
939
1323
|
const acIconPaths = {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1324
|
+
/** mdi:snowflake */
|
|
1325
|
+
cold: "m20.79 13.95l-2.33.62l-2-1.13v-2.88l2-1.13l2.33.62l.52-1.93l-1.77-.47l.46-1.77l-1.93-.52l-.62 2.33l-2 1.13L13 7.38V5.12l1.71-1.71L13.29 2L12 3.29L10.71 2L9.29 3.41L11 5.12v2.26L8.5 8.82l-2-1.13l-.58-2.33L4 5.88l.47 1.77l-1.77.47l.52 1.93l2.33-.62l2 1.13v2.89l-2 1.13l-2.33-.62l-.52 1.93l1.77.47L4 18.12l1.93.52l.62-2.33l2-1.13L11 16.62v2.26l-1.71 1.71L10.71 22L12 20.71L13.29 22l1.41-1.41l-1.7-1.71v-2.26l2.5-1.45l2 1.13l.62 2.33l1.88-.51l-.47-1.77l1.77-.47zM9.5 10.56L12 9.11l2.5 1.45v2.88L12 14.89l-2.5-1.45z",
|
|
1326
|
+
/** mdi:white-balance-sunny */
|
|
1327
|
+
hot: "m3.55 19.09l1.41 1.41l1.8-1.79l-1.42-1.42M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6s6-2.69 6-6c0-3.32-2.69-6-6-6m8 7h3v-2h-3m-2.76 7.71l1.8 1.79l1.41-1.41l-1.79-1.8M20.45 5l-1.41-1.4l-1.8 1.79l1.42 1.42M13 1h-2v3h2M6.76 5.39L4.96 3.6L3.55 5l1.79 1.81zM1 13h3v-2H1m12 9h-2v3h2",
|
|
1328
|
+
/** mdi:power */
|
|
1329
|
+
power: "m16.56 5.44l-1.45 1.45A5.97 5.97 0 0 1 18 12a6 6 0 0 1-6 6a6 6 0 0 1-6-6c0-2.17 1.16-4.06 2.88-5.12L7.44 5.44A7.96 7.96 0 0 0 4 12a8 8 0 0 0 8 8a8 8 0 0 0 8-8c0-2.72-1.36-5.12-3.44-6.56M13 3h-2v10h2",
|
|
1330
|
+
/** mdi:plus */
|
|
1331
|
+
plus: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6z",
|
|
1332
|
+
/** mdi:minus */
|
|
1333
|
+
minus: "M19 13H5v-2h14z",
|
|
1334
|
+
/** mdi:menu-up */
|
|
1335
|
+
increase: "m7 15l5-5l5 5z",
|
|
1336
|
+
/** mdi:menu-down */
|
|
1337
|
+
decrease: "m7 10l5 5l5-5z",
|
|
1338
|
+
/** mdi:fan */
|
|
1339
|
+
fan: "M12 11a1 1 0 0 0-1 1a1 1 0 0 0 1 1a1 1 0 0 0 1-1a1 1 0 0 0-1-1m.5-9c4.5 0 4.61 3.57 2.25 4.75c-.99.49-1.43 1.54-1.62 2.47c.48.2.9.51 1.22.91c3.7-2 7.68-1.21 7.68 2.37c0 4.5-3.57 4.6-4.75 2.23c-.5-.99-1.56-1.43-2.49-1.62c-.2.48-.51.89-.91 1.23c1.99 3.69 1.2 7.66-2.38 7.66c-4.5 0-4.59-3.58-2.23-4.76c.98-.49 1.42-1.53 1.62-2.45c-.49-.2-.92-.52-1.24-.92C5.96 15.85 2 15.07 2 11.5C2 7 5.56 6.89 6.74 9.26c.5.99 1.55 1.42 2.48 1.61c.19-.48.51-.9.92-1.22C8.15 5.96 8.94 2 12.5 2",
|
|
1340
|
+
/** mdi:arrow-oscillating */
|
|
1341
|
+
swing: "M6 14h3l-4 4l-4-4h3c0-2.7 1.7-7.4 7-7.9v2c-3.4.5-5 3.8-5 5.9m14 0c0-2.7-1.7-7.4-7-7.9v2c3.4.6 5 3.8 5 5.9h-3l4 4l4-4z",
|
|
1342
|
+
/** mdi:volume-high */
|
|
1343
|
+
soundNormal: "M14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.84-5 6.7v2.07c4-.91 7-4.49 7-8.77s-3-7.86-7-8.77M16.5 12c0-1.77-1-3.29-2.5-4.03V16c1.5-.71 2.5-2.24 2.5-4M3 9v6h4l5 5V4L7 9z",
|
|
1344
|
+
/** mdi:weather-night */
|
|
1345
|
+
soundSleep: "m17.75 4.09l-2.53 1.94l.91 3.06l-2.63-1.81l-2.63 1.81l.91-3.06l-2.53-1.94L12.44 4l1.06-3l1.06 3zm3.5 6.91l-1.64 1.25l.59 1.98l-1.7-1.17l-1.7 1.17l.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85c-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14c.4-.4.82-.76 1.27-1.08c.75-.53 1.93.36 1.85 1.19c-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82c-2.81 3.14-2.7 7.96.31 10.98c3.02 3.01 7.84 3.12 10.98.31",
|
|
1346
|
+
/** mdi:volume-off */
|
|
1347
|
+
soundMute: "M12 4L9.91 6.09L12 8.18M4.27 3L3 4.27L7.73 9H3v6h4l5 5v-6.73l4.25 4.26c-.67.51-1.42.93-2.25 1.17v2.07c1.38-.32 2.63-.95 3.68-1.81L19.73 21L21 19.73l-9-9M19 12c0 .94-.2 1.82-.54 2.64l1.51 1.51A8.9 8.9 0 0 0 21 12c0-4.28-3-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71m-2.5 0c0-1.77-1-3.29-2.5-4.03v2.21l2.45 2.45c.05-.2.05-.42.05-.63",
|
|
1348
|
+
/** mdi:leaf */
|
|
1349
|
+
leaf: "M17 8C8 10 5.9 16.17 3.82 21.34l1.89.66l.95-2.3c.48.17.98.3 1.34.3C19 20 22 3 22 3c-1 2-8 2.25-13 3.25S2 11.5 2 13.5s1.75 3.75 1.75 3.75C7 8 17 8 17 8"
|
|
948
1350
|
};
|
|
1351
|
+
//#endregion
|
|
1352
|
+
//#region src/ui.ts
|
|
949
1353
|
/**
|
|
950
1354
|
* 声音键在各声音模式下显示的图标。
|
|
951
1355
|
*/
|
|
@@ -955,13 +1359,42 @@ const acSoundIcons = {
|
|
|
955
1359
|
mute: "soundMute"
|
|
956
1360
|
};
|
|
957
1361
|
/**
|
|
958
|
-
*
|
|
1362
|
+
* 遥控器小屏的读屏文字与各按键的悬停提示,保证各框架文案一致。
|
|
1363
|
+
*/
|
|
1364
|
+
function getAcRemoteText(state) {
|
|
1365
|
+
const swing = `扫风:${state.swing ? "开" : "关"}`;
|
|
1366
|
+
const sound = `声音:${acSoundModeLabels[state.sound]}`;
|
|
1367
|
+
const fan = `风速:${acFanSpeedLabels[state.fan ?? "auto"]}`;
|
|
1368
|
+
return {
|
|
1369
|
+
display: `${state.status ? "运行" : "待机"},${acModeLabels[state.mode]} ${state.temperature}°C,${fan},${swing},${sound}`,
|
|
1370
|
+
power: `电源:${state.status ? "开" : "关"}`,
|
|
1371
|
+
mode: acModeLabels,
|
|
1372
|
+
fan,
|
|
1373
|
+
swing,
|
|
1374
|
+
sound
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* 能效标签的点阵与等级条布局:蓝白背景的「中国能效标识」,带能效信息码
|
|
1379
|
+
* (二维码),样式依据《房间空气调节器能源效率标识实施规则》(CEL 010—2019)。
|
|
1380
|
+
*
|
|
1381
|
+
* 等级按现行 GB 21455-2019 分 5 级,1 级最高。GB 21455 的修订版征求意见稿
|
|
1382
|
+
* (2026 年 5 月)拟改为 3 级、以全年动态能源消耗效率(DAPF)分级;正式发布并
|
|
1383
|
+
* 实施后,再更新 `grades`、`standard` 与 `levels`。
|
|
1384
|
+
*
|
|
1385
|
+
* - `top`:标识名称(白点),右侧是能效信息码
|
|
1386
|
+
* - 等级区:首行是 1 级绿色条和指示箭头,`levels` 是 2–5 级
|
|
1387
|
+
* - `detail`:生产者、型号、APF 等参数(黑点)
|
|
1388
|
+
* - `footer`:依据的国家标准编号(白点)
|
|
959
1389
|
*/
|
|
960
1390
|
const acEnergyLabel = {
|
|
1391
|
+
grade: 1,
|
|
1392
|
+
grades: 5,
|
|
1393
|
+
standard: "GB 21455-2019",
|
|
961
1394
|
top: [{
|
|
962
|
-
num:
|
|
1395
|
+
num: 4,
|
|
963
1396
|
size: 4,
|
|
964
|
-
mx:
|
|
1397
|
+
mx: 1.5,
|
|
965
1398
|
my: 0,
|
|
966
1399
|
color: "white"
|
|
967
1400
|
}],
|
|
@@ -1001,7 +1434,7 @@ const acEnergyLabel = {
|
|
|
1001
1434
|
width: "50%"
|
|
1002
1435
|
},
|
|
1003
1436
|
{
|
|
1004
|
-
background: "#
|
|
1437
|
+
background: "#fdd835",
|
|
1005
1438
|
width: "60%"
|
|
1006
1439
|
},
|
|
1007
1440
|
{
|
|
@@ -1014,5 +1447,11 @@ const acEnergyLabel = {
|
|
|
1014
1447
|
}
|
|
1015
1448
|
]
|
|
1016
1449
|
};
|
|
1450
|
+
/**
|
|
1451
|
+
* 能效标签的读屏文字。
|
|
1452
|
+
*/
|
|
1453
|
+
function getAcEnergyLabelText() {
|
|
1454
|
+
return `中国能效标识:能效等级 ${acEnergyLabel.grade} 级(共 ${acEnergyLabel.grades} 级),依据 ${acEnergyLabel.standard}`;
|
|
1455
|
+
}
|
|
1017
1456
|
//#endregion
|
|
1018
|
-
export { acAudioFileNames, acEnergyLabel, acIconPaths, acMaxTilt, acModeIcons, acMotion, acReducedMotionDuration, acReducedMotionQuery, acReducer, acShellSize, acSoundIcons, acSoundModeLabels, acSoundModes, acSoundVolumes, acStorageKey, acTone, celsiusToHeatLevel, clampTemperature, createAcAudioController, createAirConditionerStore, createComputePressureSource, createHeatGovernor, createMemoryStorage, defaultAcLimits, defaultAcState, defaultHeatThresholds, getAcActionNotification, getAcScale, getBrowserStorage, getRecommendedTemperatureMessage, getSoundModeMessage, heatLevelRank, heatLevels, isAcSoundMode, isHeatLevel, maxTemperature, minTemperature, nextAcSoundMode, normalizeAcState, observeAcScale, observeAcTilt, planCooling, prefersReducedMotion, readAcState, recommendedColdTemperature, recommendedHotTemperature, renderAcMotionCssVars, resolveAcAudioAssets, soundTimings, writeAcState };
|
|
1457
|
+
export { acAirflowAnchors, acAudioFileNames, acDripIntervalRange, acDripMaxTemperature, acEnergyLabel, acFanBars, acFanSoundLevels, acFanSpeedLabels, acFanSpeeds, acFormLabels, acForms, acIconPaths, acLouverMotion, acMaxTilt, acModeIcons, acModeLabels, acModelColors, acModelFiles, acModelParts, acMotion, acReducedMotionDuration, acReducedMotionQuery, acReducer, acShellSize, acSoundIcons, acSoundModeLabels, acSoundModes, acSoundVolumes, acStorageKey, acTone, bindAcPressRepeat, celsiusToHeatLevel, clampTemperature, createAcAudioController, createAcNotificationPresenter, createAirConditionerStore, createComputePressureSource, createHeatGovernor, createMemoryStorage, defaultAcLimits, defaultAcState, defaultHeatThresholds, getAcActionNotification, getAcCondensationStyle, getAcDripInterval, getAcEnergyLabelText, getAcLedColor, getAcMist, getAcRemoteText, getAcScale, getBrowserStorage, getEnergyTip, getFanSpeedMessage, getRecommendedTemperatureMessage, getSoundModeMessage, getSwingMessage, heatLevelRank, heatLevels, isAcFanSpeed, isAcForm, isAcSoundMode, isHeatLevel, maxTemperature, minTemperature, nextAcFanSpeed, nextAcSoundMode, normalizeAcState, observeAcScale, observeAcTilt, planCooling, prefersReducedMotion, readAcState, recommendedColdTemperature, recommendedHotTemperature, renderAcMotionCssVars, resolveAcAudioAssets, soundTimings, writeAcState };
|